race problem with mocha unit testing Firestore onSnapshot, test ends before onSnapshot returns data - google-cloud-firestore

I have a function I'm testing. It's called from the constructor on an object.
constructor(authorID: string) {
this.loadRecipes(authorID)
}
private loadRecipes = (author_id: string) => {
const first = query(collection(db, "cookbook"), where("author_id", "==", author_id));
this._unsubscribe = onSnapshot(first, (querySnapshot) => {
this._recipes = [];
querySnapshot.forEach((doc) => {
this._recipes.push(new Recipe(doc.id, doc.data().author_id, "", "", -1, "draft", [], [], [], 0, ""));
});
});
};
I'm calling it from a mocha test:
it("Creates a recipe list by author ID", () => {
authorRecipes = new RecipeList(author_id);
assert(authorRecipes.list.length>0);
});
The code works fine, but the test fails. The problem is that the test completes running long before the onSnapshot ever fires and populates the list. Is there a way to force the query to populate - sort of like an async/await? Or a way to force mocha to wait? Setting a breakpoint & debugging, the onSnapshot is eventually called so the code works. I just cannot do any follow on tests because the data isn't there yet.

I ended up adding a "loading" flag to the class:
private loadRecipes = (author_id: string, _max?: number, _start?: number) => {
this._loading = true;
const first = query(collection(db, "cookbook"), where("author_id", "==", author_id));
this._unsubscribe = onSnapshot(first, (querySnapshot) => {
this._recipes = [];
querySnapshot.forEach((doc) => {
this._recipes.push(new Recipe(doc.id, doc.data().author_id, "", "", -1, "draft", [], [], [], 0, ""));
});
this._loading = false;
});
};
And then watched the flag with a delay in the test:
it("Creates a recipe list by author ID", async () => {
authorRecipes = new RecipeList(author_id);
while (authorRecipes.loading) {
await timeout(1000);
}
assert(!authorRecipes.loading);
});
Not super elegant, but gets the job done.

Related

Failed to add new elements when set initialState as an empty object

I try to use redux toolkit and I have this as menu-slice.js
I try to use property accessors to add a new property to fileItems, its initial value is an empty object.
import { createSlice } from "#reduxjs/toolkit";
const menuSlice = createSlice({
name: "ui",
initialState: {
fileItems: {},
},
reducers: {
setFileDate: (state, action) => {
state.FileDate = action.payload;
},
replaceFileItems: (state, action) => {
const filesList = action.payload.map((fileName) =>
fileName.slice(fileName.indexOf("/") + 1)
);
state.fileItems[state.FileDate] = filesList;
console.log(`filesList: ${filesList}`);
console.log(`state.fileItems: ${JSON.stringify(state.fileItems)}`);
console.log(`state.FileDate: ${state.FileDate}`);
state.fileContents = null;
},
I call dispatch with the api return value ( dispatch(menuActions.replaceFileItems(fileResponse.data));)
in menu-action.js:
the return value is an array of strings.
export const fetchFiles = (fileDate) => {
return async (dispatch) => {
const fetchFilesList = async () => {
const response = await fetch(
"some url" +
new URLSearchParams({
env: "https://env.com",
date: fileDate,
})
);
if (!response.ok) {
throw new Error("Fail to fetch files list!");
}
const data = await response.json();
return data;
};
try {
const fileResponse = await fetchFilesList();
dispatch(menuActions.setFileDate(FileDate));
dispatch(menuActions.replaceFileItems(fileResponse.data));
} catch (error) {
dispatch(
menuActions.showNotification({....
})
);
}
};
};
But it never prints console logs and didn't display where went wrong in the console or in the chrome redux extension.
I want to add data into state.fileItems on each click that triggers fetchFiles() when it returns a new array:
from state.fileItems = {}
check if state.fileItems already has the date as key,
if not already has the date as key,
change to ex: state.fileItems = {"2022-01-01": Array(2)}
and so on..
ex: state.fileItems = { "2022-01-01": Array(2), "2022-01-02": Array(2) }
I also tried to set state.fileItems as an empty array, and use push, but it didn't work either, nothing printed out, state.fileItems value was always undefined.
Can anyone please tell me why this didn't work?
Thanks for your time to read my question.

Updating sub document using save() method in mongoose does not get saved in database and shows no error

I have a Mongoose model like this:
const centerSchema = mongoose.Schema({
centerName: {
type: String,
required: true,
},
candidates: [
{
candidateName: String,
voteReceived: {
type: Number,
default: 0,
},
candidateQR: {
type: String,
default: null,
},
},
],
totalVote: {
type: Number,
default: 0,
},
centerQR: String,
});
I have a Node.JS controller function like this:
exports.createCenter = async (req, res, next) => {
const newCenter = await Center.create(req.body);
newCenter.candidates.forEach(async (candidate, i) => {
const candidateQRGen = await promisify(qrCode.toDataURL)(
candidate._id.toString()
);
candidate.candidateQR = candidateQRGen;
// ** Tried these: **
// newCenter.markModified("candidates." + i);
// candidate.markModified("candidateQR");
});
// * Also tried this *
// newCenter.markModified("candidates");
const upDatedCenter = await newCenter.save();
res.status(201).json(upDatedCenter);
};
Simply, I want to modify the candidateQR field on the subdocument. The result should be like this:
{
"centerName": "Omuk Center",
"candidates": [
{
"candidateName": "A",
"voteReceived": 0,
"candidateQR": "some random qr code text",
"_id": "624433fc5bd40f70a4fda276"
},
{
"candidateName": "B",
"voteReceived": 0,
"candidateQR": "some random qr code text",
"_id": "624433fc5bd40f70a4fda277"
},
{
"candidateName": "C",
"voteReceived": 0,
"candidateQR": "some random qr code text",
"_id": "624433fc5bd40f70a4fda278"
}
],
"totalVote": 0,
"_id": "624433fc5bd40f70a4fda275",
"__v": 1,
}
But I am getting the candidateQR still as null in the Database. I tried markModified() method. But that didn't help (showed in the comment section in the code above). I didn't get any error message. In response I get the expected result. But that result is not being saved on the database. I just want candidateQR field to be changed. But couldn't figure out how.
forEach loop was the culprit here. After replacing the forEach with for...of it solved the issue. Basically, forEach takes a callback function which is marked as async in the codebase which returns a Promise initially and gets executed later.
As for...of doesn't take any callback function so the await inside of it falls under the controller function's scope and gets executed immediately. Thanks to Indraraj26 for pointing this out. So, the final working version of the controller would be like this:
exports.createCenter = async (req, res, next) => {
const newCenter = await Center.create(req.body);
for(const candidate of newCenter.candidates) {
const candidateQRGen = await promisify(qrCode.toDataURL)(
candidate._id.toString()
);
candidate.candidateQR = candidateQRGen;
};
newCenter.markModified("candidates");
const upDatedCenter = await newCenter.save();
res.status(201).json(upDatedCenter);
};
Also, shoutout to Moniruzzaman Dipto for showing a different approach to solve the issue using async.eachSeries() method.
You can use eachSeries instead of the forEach loop.
const async = require("async");
exports.createCenter = async (req, res, next) => {
const newCenter = await Center.create(req.body);
async.eachSeries(newCenter.candidates, async (candidate, done) => {
const candidateQRGen = await promisify(qrCode.toDataURL)(
candidate._id.toString(),
);
candidate.candidateQR = candidateQRGen;
newCenter.markModified("candidates");
await newCenter.save(done);
});
res.status(201).json(newCenter);
};
As far as I understand, you are just looping through the candidates array but you
are not storing the updated array. You need to store the updated data in a variable as well. Please give it a try with the solution below using map.
exports.createCenter = async (req, res, next) => {
const newCenter = await Center.create(req.body);
let candidates = newCenter.candidates;
candidates = candidates.map(candidate => {
const candidateQRGen = await promisify(qrCode.toDataURL)(
candidate._id.toString()
);
return {
...candidate,
candidateQR: candidateQRGen
}
});
newCenter.candidates = candidates;
const upDatedCenter = await newCenter.save();
res.status(201).json(upDatedCenter);
};
You can use this before save()
newCenter.markModified('candidates');

DocumentReference#onSnapshot event firing when data added even though it doesn't match the query

I have a collection "users" in firestore. Each doc has a username as its key and has a collection called "notifications" inside.
The following code runs upon app load, presumably setting up an event listener of sorts to detect when changes occur.
const notificationRef = this.$db.collection(USERS).doc(this.currentUser().userName).collection("notifications");
notificationRef
.onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
switch (change.type) {
case "added":
case "modified":
console.log("Detected new or updated notification: ", change.doc.data());
this.$store.commit("setNotification", {key: change.doc.id, ...change.doc.data()});
break;
case "removed":
console.log("Detected deleted notification: ", change.doc.data());
this.$store.commit("removeNotification", change.doc.id);
break;
default:
break;
}
});
this.$forceUpdate();
});
Here is how I am adding notifications to the db.
async sendInvites() {
var invitedMembers = [];
var notInvited = [];
var batch = this.$db.batch();
const userQuery = this.$db.collection(USERS).where('email', 'in', Object.values(this.data));
// We have to 'await' on this so that we can use the batch write
await userQuery
.get()
.then(snapshot => {
snapshot.forEach(doc => {
// Make sure we don't invite the current user!
if (doc.data().email !== this.currentUser().email) {
invitedMembers.push(doc.data().email);
const currentUser = this.currentUser();
batch.set(doc.ref.collection('notifications').doc(), {
text: `Invited you to join "${this.currentGroup.value.name}"`,
action: "invite",
by: {
userName: currentUser.userName,
displayName: currentUser.displayName,
photoURL: currentUser.photoURL
},
group: this.currentGroup.key
});
}
});
});
batch.commit().then(() => {
Object.values(this.data).forEach(value => {
if (!invitedMembers.includes(value)) {
if (value !== "") {
notInvited.push(value);
}
}
});
console.log("Invited members: ", invitedMembers);
console.log("Not invited: ", notInvited);
if (notInvited.length > 0) {
this.errors.notInvited = `The following members could not be found and were not invited: ${notInvited}`;
}
});
}
Scenario:
Current User: kylon
Notification added to: tyner
The problem is, if I add a notification to tyner while kylon is currently logged in, the notification shows up as an add and is therefore being displayed to kylon's frontend (only initially, when I reload the page, it loads in the correct documents).
I have checked firestore and it is saving in the correct spot ("users/tyner/notifications/").
Since I am querying for the user doc with the current user's username "kylon", I would assume that firestore would only provide snapshots that satisfy that criteria, but that is not the case.
Could anyone shed some light on why this is happening?

How to properly use jasmine-marbles to test multiple actions in ofType

I have an Effect that is called each time it recives an action of more than one "kind"
myEffect.effect.ts
someEffect$ = createEffect(() =>
this.actions$.pipe(
ofType(fromActions.actionOne, fromActions.actionTwo),
exhaustMap(() => {
return this.myService.getSomeDataViaHTTP().pipe(
map((data) =>
fromActions.successAction({ payload: data})
),
catchError((err) =>
ObservableOf(fromActions.failAction({ payload: err }))
)
);
})
)
);
in my test I tried to "simulate the two different actions but I always end up with an error, while if I try with one single action it works perfectly
The Before Each part
describe('MyEffect', () => {
let actions$: Observable<Action>;
let effects: MyEffect;
let userServiceSpy: jasmine.SpyObj<MyService>;
const data = {
// Some data structure
};
beforeEach(() => {
const spy = jasmine.createSpyObj('MyService', [
'getSomeDataViaHTTP',
]);
TestBed.configureTestingModule({
providers: [
MyEffect,
provideMockActions(() => actions$),
{
provide: MyService,
useValue: spy,
},
],
});
effects = TestBed.get(MyEffect);
userServiceSpy = TestBed.get(MyService);
});
This works perfectly
it('should return successActionsuccessAction', () => {
const action = actionOne();
const outcome = successAction({ payload: data });
actions$ = hot('-a', { a: action });
const response = cold('-a|', { a: data });
userServiceSpy.getSomeDataViaHTTP.and.returnValue(response);
const expected = cold('--b', { b: outcome });
expect(effects.someEffect$).toBeObservable(expected);
});
This doesn't work
it('should return successAction', () => {
const actions = [actionOne(), actionTwo()];
const outcome = successAction({ payload: data });
actions$ = hot('-a-b', { a: actions[0], b: actions[1] });
const response = cold('-a-a', { a: data });
userServiceSpy.getSomeDataViaHTTP.and.returnValue(response);
const expected = cold('--b--b', { b: outcome });
expect(effects.someEffect$).toBeObservable(expected);
});
There are two problems in this code.
It suggests that getSomeDataViaHTTP returns two values. This is wrong, the response is no different from your first example: '-a|'
It expects the second successAction to appear after 40 ms (--b--b, count the number of dashes). This is not correct, because actionTwo happens after 20 ms (-a-a) and response takes another 10 ms (-a). So the first successAction is after 20ms (10+10), the second is after 30ms (20+10). The marble is: '--b-b'.
Input actions : -a -a
1st http response : -a
2nd http response : -a
Output actions : --b -b
The working code:
it('should return successAction', () => {
const actions = [actionOne(), actionTwo()];
actions$ = hot('-a-b', { a: actions[0], b: actions[1] });
const response = cold('-a|', { a: data });
userServiceSpy.getSomeDataViaHTTP.and.returnValue(response);
const outcome = successAction({ payload: data });
const expected = cold('--b-b', { b: outcome });
expect(effects.someEffect$).toBeObservable(expected);
});
Marble testing is cool but it involves some black magic you should prepare for. I'd very much recommend you to carefully read this excellent article to have a deeper understanding of the subject.

How to invoke openwhisk action within openwhisk platform on bluemix?

I have created two actions on OpenWhisk on Bluemix. Both independently work fine when I can call them from outside the OpenWhisk platform. But I want to call action1 from within action2, and am using the following syntax:
var openwhisk = require('openwhisk');
function main(args){
const name = 'action2';
const blocking = true;
const params = { param1: 'sthing'};
var ow = openwhisk();
ow.actions.invoke({name, blocking, params})
.then(result => {
console.log('result: ', result);
return result; // ?
}).catch(err => {
console.error('failed to invoke actions', err);
});
}
But I get an empty result and no console messages. Some help would be great.
Update1:
When adding as suggested the return option, to return the Promise of OpenWhisk, as follows:
return ow.actions.invoke({name, blocking, params})
.then(result => {
console.log('result: ', result);
return result;
}).catch(err => {
console.error('failed to invoke actions', err);
throw err;
});
the response value of action2 is not as expected but contains:
{ "isFulfilled": false, "isRejected": false }
where I expect the return message of action2 (which reads a Google Sheets API) and parses the result:
{
"duration": 139,
"name": "getEventCfps",
"subject": "me#email.com",
...
"response": {
"result": {
"message": [
{
"location": "Atlanta, GA",
"url": "https://werise.tech/",
"event": "We RISE Women in Tech Conference",
"cfp-deadline": "3/31/2017",
...
}
]
},
"success": true,
"status": "success"
},
...
}
So I am expecting I am not parsing the '.then(result' variable in action1 correctly? cause when I test action2 separately, from outside OpenWhisk via Postman or API Connect, or directly by 'Run this action' in OpenWhisk/Bluemix it returns the correct values.
Update2:
Alright solved. I was calling the ow.actions.invoke to action2 in a function that was called within the action1, this nesting of returns, caused the issue. When I moved the invoke code directly in the main function, all resolved as expected. Double trouble when nesting promises and returns. Mea culpa. Thanks everyone
You need to return a Promise in your function try this
var openwhisk = require('openwhisk');
function main(args){
const name = '/whisk.system/utils/echo';
const blocking = true;
const params = { param1: 'sthing'};
var ow = openwhisk();
return ow.actions.invoke({name, blocking, params})
.then(result => {
console.log('result: ', result);
return result;
}).catch(err => {
console.error('failed to invoke actions', err);
throw err;
});
}
If you just want to invoke the action:
var openwhisk = require('openwhisk');
function main(args) {
var ow = openwhisk();
const name = args.action;
const blocking = false
const result = false
const params = args;
ow.actions.invoke({
name,
blocking,
result,
params
});
return {
statusCode: 200,
body: 'Action ' + name + ' invoked successfully'
};
}
If you want to wait for the result of the invoked action:
var openwhisk = require('openwhisk');
function main(args) {
var ow = openwhisk();
const name = args.action;
const blocking = false
const result = false
const params = args;
return ow.actions.invoke({
name,
blocking,
result,
params
}).then(function (res) {
return {
statusCode: 200,
body: res
};
});
}