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

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.

Related

Redux toolkit query. useLazyQuery

Try to understand how to structure queries.
What I have now:
File for CRUD:
export const PromoService = apiClient.injectEndpoints({
endpoints: (build) => ({
fetchPromoById: build.query<
Promotion,
{ ppeType: PpeType; id: string }
>({
query: ({ ppeType, id }) => apiQuery(ppeType, 'fetchPromoById', id),
providesTags: (_result, _err) => [{ type: 'Promo' }],
}),
fetchPromoByCategory: build.mutation<
PromotionData,
{ ppeType: PpeType; type: string; bannerId: string }
>({
query: ({ ppeType, type, bannerId }) => ({
url: apiQuery(ppeType, 'fetchPromoByCategory'),
method: 'POST',
body: fetchPromoByCategoryBody(type, bannerId),
}),
invalidatesTags: ['Promo'],
}),
}),
});
export const { useLazyFetchPromoByIdQuery, useFetchPromoByCategoryMutation } =
PromoService;
File for slices:
const initialState: PromotionState = {
chosenPromotion: {} as Promotion,
promoList: [],
};
const promoSlice = createSlice({
name: 'promo',
initialState,
reducers: {
setChosenPromotion: (state, action: PayloadAction<Promotion>) => {
state.chosenPromotion = action.payload;
},
setPromoList: (state, action: PayloadAction<Promotion[]>) => {
state.promoList = action.payload;
},
},
});
Component:
const [fetchPromoByCategory, { isLoading, data: categoryData }] =
useFetchPromoByCategoryMutation({
fixedCacheKey: 'shared-update-promo',
});
const [trigger, result] = useLazyFetchPromoByIdQuery();
const chosenPromo = result.data;
useEffect(() => {
chosenPromo && dispatch(setChosenPromotion(chosenPromo));
}, [chosenPromo]);
There is no problem get data from useMutation in different components skipping the stage of store data via reducer.
Just use fixedCacheKey and it works fine.
Is it possible to use similar approach for getting data in different components with useLazyQuery?
I use additional dispatch to store data from useLazyQuery but I'm sure it's not appropriate approach.
It is perfectly valid to have multiple different query cache entries at once, so useLazyQuery will not initialize to one of them - it will get it's arguments once you call the trigger function.
It looks like you should use useQuery here, sometimes with the skip parameter when you don't want anything fetched from the start.

node-opcua subscribe to event and get all fields

i've found here an example code that works ok as a subscription to events (code below),
i'm trying to use it on a custom serve that send some other fields, but if i add those fileds name to the const fields, i get null value, so i would like to get all the fields available in the event to understand what the server send but if i remove the eventFilter option i just get empty events
any suggestion on how to get all the fields from an event?
import {
AttributeIds,
constructEventFilter,
ObjectIds,
OPCUAClient,
TimestampsToReturn,
Variant,
} from "node-opcua-client";
async function main(): Promise<void> {
const client = OPCUAClient.create({});
const endpointUrl = "opc.tcp://opcua.demo-this.com:62544/Quickstarts/AlarmConditionServer";
const subscriptionParamters = {
requestedPublishingInterval: 1000,
maxNotificationsPerPublish: 100,
publishingEnabled: true,
priority: 10,
};
await client.withSubscriptionAsync(endpointUrl, subscriptionParamters, async (session, subscription) => {
const fields = [
"EventId",
"EventType",
"SourceNode",
"SourceName",
"Time",
"ReceiveTime",
"Message",
"Severity",
];
const eventFilter = constructEventFilter(fields);
const event_monitoringItem = await subscription.monitor(
{
nodeId: ObjectIds.Server,
attributeId: AttributeIds.EventNotifier,
},
{
queueSize: 10,
filter: eventFilter,
discardOldest: true,
},
TimestampsToReturn.Both
);
event_monitoringItem.on("changed", (events: Variant[]) => {
for(let i=0;i<events.length;i++) {
console.log(fields[i],"=", events[i].toString());
}
console.log("----------------\n\n")
});
console.log("CTRL+C to stop");
await new Promise<void>((resolve) => process.once("SIGINT", resolve));
}
);
}
main();
I think you have to add the namespace index for your single event fields as well. And if they are sub properties the full path.
So it could look like this:
const fields = [
"EventId",
"EventType",
"3:MyProp1",
"3:MyProp2",
"3:SomeSubObject/SomeOtherProperty",
];

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');

Call OPCUA method with struct input argument using node-opcua

I am trying to interface with an RFID reader which implements an OPC-UA server according to this specification.
I am trying to call the method ScanStart which takes the ScanSettings struct as an input argument (an AutoID datatype) but despite reading through the examples and documentation I can't figure out a way to do this.
Using UAExpert I can call the method and enter the values for the struct using the GUI which produces the following dump in wireshark:
ArraySize: 1
[0]: Variant
Variant Type: ExtensionObject (0x16)
Value: ExtensionObject
TypeId: ExpandedNodeId
EncodingMask: 0x01, EncodingMask: Four byte encoded Numeric
.... 0001 = EncodingMask: Four byte encoded Numeric (0x1)
.0.. .... = has server index: False
0... .... = has namespace uri: False
Namespace Index: 3
Identifier Numeric: 5015
EncodingMask: 0x01, has binary body
.... ...1 = has binary body: True
.... ..0. = has xml body: False
ByteString: 0000000000000000000000000000000000
Has anyone successfully managed to register an ExtensionObject for passing to a method call using node-opcua? At this point I am happy to just send the ByteString above without needing to encode/decode the struct as it is always static.
Apparently there is a constructExtensionObject method. The client code I have for this is:
(async () => {
const client = OPCUAClient.create({ endpoint_must_exist: false});
client.on("backoff", () => console.log("Backoff: trying to connect to ", endpointUri));
await client.withSessionAsync(endpointUri, async (session) => {
let scanSettings = {
Duration: 0,
Cyles: 0,
DataAvailble: false
};
const nodeID = new NodeId(NodeIdType.STRING, "rfr310.ScanStart.InputArguments", 4);
const extObj = session.constructExtensionObject(nodeID, scanSettings);
const methodsToCall = [
{
objectId: "ns=4;s=rfr310",
methodId: "ns=4;s=rfr310.ScanStart",
inputArguments: [extObj]
}
];
extObj.then(() => {
session.call(methodsToCall,(err,results) => {
if (err) {
console.log(err);
} else {
console.log(results);
}
});
}).catch(() => {
})
});
})();
produces the error "dispose when pendingTransactions is not empty", which is caught by the extObj.catch()
What am I doing wrong? I'm fairly certain this is a promise handling issue on my part...
Any help is appreciated!
OK so I finally got there. Here is the method to call an OPC-UA method with a struct input argument using node-opcua:
const { OPCUAClient, NodeId, NodeIdType, DataType} = require("node-opcua");
const endpointUri = "opc.tcp://<your-endpoint>:<your-port>";
(async () => {
const client = OPCUAClient.create({ endpoint_must_exist: false});
client.on("backoff", () => console.log("Backoff: trying to connect to ", endpointUri));
await client.withSessionAsync(endpointUri, async (session) => {
// Scan settings value input
const scanSettingsParams = {
duration: 0,
cycles : 0,
dataAvailable : false,
locationType: 0
};
try {
// NodeID for InputArguments struct type (inherits from ScanSettings)
const nodeID = new NodeId(NodeIdType.NUMERIC, 3010, 3);
// Create ExtensionObject for InputArguments
const scanSettingsObj = await session.constructExtensionObject(nodeID, scanSettingsParams);
// Populate Method call with ExtensionObject as InputArgument
const methodToCall = {
objectId: "ns=4;s=rfr310",
methodId: "ns=4;s=rfr310.ScanStart",
inputArguments: [
{
dataType: DataType.ExtensionObject,
value: scanSettingsObj
}
]
};
// Call method, passing ScanSettings as input argument
session.call(methodToCall,(err,results) => {
if (err) {
console.log(err);
} else {
console.log(results);
}
});
} catch (err) {
console.log(err);
}
});
})();