I'm having trouble mocking the call of an individually imported function to my tests. The test is a simple function that I put within my Redux actions to be able to set a variable based on a condition.
Here's the function in Body.duck.js:
export const getCurrentOrPrevSelection = isExecutedFromPagination => (dispatch, getState) => {
const {
editor: { selection },
body: { queryRequest },
} = getState();
if (isExecutedFromPagination && queryRequest.breadcrumb) {
const {
query: { branch, includeSplits, primaryFa, split, isInitial },
} = queryRequest.breadcrumb;
return {
branch,
includeSplits,
primaryFa,
split,
isInitial,
};
}
return selection;
};
And here's the test file:
import reudcer, { ...other exported functions, getCurrentOrPrevSelection } from '../Body.duck';
it ('should use selection in breadcrumb state when fetching new data from pagination action', () => {
let isExecutedFromPagination = false;
const bodyState = {
...initialState.body,
queryRequest: {
...initialState.body.queryRequest,
breadcrumb: {
...initialState.body.breadcrumb,
query: {
name: 'Full Book Performance',
branch: null,
includeSplits: true,
primaryFa: 'AXFO',
split: null,
isInitial: true,
},
},
},
};
const selection = {
branch: null,
includeSplits: true,
primaryFa: 'AXFO',
split: null,
isInitial: true,
};
expect(getCurrentOrPrevSelection(isExecutedFromPagination)(jest.fn(), () => ({
body: { ...bodyState },
editor: { faidSelection },
}))).toHaveReturnedWith({
branch: null,
includeSplits: true,
primaryFa: 'AXFO',
split: null,
isInitial: true,
});
});
If I don't include any sort of mock reference to getCurrentOrPrevSelection, I get this error below, but it returns the correct value as expected:
expect(jest.fn())[.not].toHaveReturnedWith()
jest.fn() value must be a mock function or spy.
Received:
object: {"branch": null, "includeSplits": true, "isInitial": true, "primaryFa": "AXFO", "split": null}
If I do something like getCurrentOrPrevFaidSelection = jest.fn();, I get an error saying getCurrentOrPrevFaidSelection is read-only
What can I do differently here?
You want to test this function. So you don't need to mock that.
Just call function and verify result with expect().toEqual or expect().toMatchObject.
expect(getCurrentOrPrevSelection(isExecutedFromPagination)(.....)).toMatchObject({
branch: null,
...
});
Also passing jest.fn() directly as argument does not really make sense: you cannot either verify it has been called or provide mock return.
const dispatchMock = jest.fn();
expect(getCurrentOrPrevSelection(isExecutedFromPagination)(dispatchMock, ....);
expect(dispatchMock).toHaveBeenCalledWith(...)
Once it's just not expected to be called as it is in your sample you better explicitly provide noop function () => {} instead of jest.fn(). This way you make it's explicit so nobody will be confused if it's expected there is no assertions against this function or not.
Offtop: actually this is not really good way to test redux action creators. See you actually test implementation details. What if you migrate from redux-thunk to redux-saga or redux-loop? Or split single action into 2 for better flexibility? By now it would mean you have to rewrite all your tests.
What if instead of testing action creator in isolation you connect action to real(not mocked) store? You could dispatch action(after mocking calls to external API) and validate store's state.
Related
I have an object in my pinia store like
import { defineStore } from "pinia";
export const useSearchStore = defineStore("store", {
state: () => {
return {
myobj: {
foo: 0,
bar: 2000,
too: 1000,
},
};
},
getters: {
changed() {
// doesn't work
return Object.entries(this.myobj).filter(([key, value]) => value != initialvalue
);
},
},
});
How do I get the initial value to test if the object changed. Or how can I return a filtered object with only those entries different from initial state?
My current workaround:
in a created hook I make a hard copy of the store object I then can compare to. I guess there is a more elegant way...
I had done this (although I do not know if there a better way to avoid cloning without duplicating your initial state).
Define your initial state outside and assign it to a variable as follows;
const initialState = {
foo: 0,
bar: 2000,
too: 1000
}
Then you can use cloning to retain the original state;
export const useSearchStore = defineStore("store", {
state: () => {
return {
myobj: structuredClone(initialState),
};
},
getters: {
changed: (state) => deepEquals(initialState, state.myobj);
},
});
where deepEquals is a method which deep compares the two objects (which you would have to implement). I would use lodash (npm i lodash and npm i #types/lodash --save-dev if you're using TypeScript) for this.
Full code (with lodash);
import { defineStore } from "pinia";
import { cloneDeep, isEqual } from "lodash";
const initialState = {
foo: 0,
bar: 2000,
too: 1000
}
export const useSearchStore = defineStore("store", {
state: () => ({
myobj: cloneDeep(initialState)
}),
getters: {
changed(state) {
return isEqual(initialState, state.myobj);
},
},
});
If you also want the differences between the two you can use the following function (the _ is lodash - import _ from "lodash");
function difference(object, base) {
function changes(object, base) {
return _.transform(object, function (result: object, value, key) {
if (!_.isEqual(value, base[key])) {
result[key] =
_.isObject(value) && _.isObject(base[key])
? changes(value, base[key])
: value;
}
});
}
return changes(object, base);
}
courtesy of https://gist.github.com/Yimiprod/7ee176597fef230d1451
EDIT:
The other way you would do this is to use a watcher to subscribe to changes. The disadvantage to this is that you either have to be OK with your state marked as "changed" if you change back the data to the initial state. Otherwise, you would have to implement a system (perhaps using a stack data structure) to maintain a list of changes so that if two changes which cancel each other out occur then you would remark the state as "unchanged". You would have to keep another variable (boolean) in the state which holds whether the state has been changed/unchanged - but this would be more complicated to implement and (depending on your use case) not worth it.
My understanding is that using serializeIds: 'always' will give me this data, but it does not.
Here's what I'm expecting:
{
id="1"
title="some title"
customerId="2"
}
Instead the output I'm receiving is:
{
id="1"
title="some title"
}
My code looks something like this:
import {
Server,
Serializer,
Model,
belongsTo,
hasMany,
Factory
} from "miragejs";
import faker from "faker";
const ApplicationSerializer = Serializer.extend({
// don't want a root prop
root: false,
// true required to have root:false
embed: true,
// will always serialize the ids of all relationships for the model or collection in the response
serializeIds: "always"
});
export function makeServer() {
let server = newServer({
models: {
invoice: Model.extend({
customer: belongsTo()
}),
customer: Model.extend({
invoices: hasMany()
})
},
factories: {
invoice: Factory.extend({
title(i) {
return `Invoice ${i}`;
},
afterCreate(invoice, server) {
if (!invoice.customer) {
invoice.update({
customer: server.create("customer")
});
}
}
}),
customer: Factory.extend({
name() {
let fullName = () =>
`${faker.name.firstName()} ${faker.name.lastName()}`;
return fullName;
}
})
},
seeds(server) {
server.createList("invoice", 10);
},
serializers: {
application: ApplicationSerializer,
invoice: ApplicationSerializer.extend({
include: ["customer"]
})
},
routes() {
this.namespace = "api";
this.get("/auth");
}
});
}
Changing the config to root: true, embed: false, provides the correct output in the invoice models, but adds the root and sideloads the customer, which I don't want.
You've run into some strange behavior with how how serializeIds interacts with embed.
First, it's confusing why you need to set embed: true when you're just trying to disable the root. The reason is because embed defaults to false, so if you remove the root and try to include related resources, Mirage doesn't know where to put them. This is a confusing mix of options and Mirage should really have different "modes" that take this into account.
Second, it seems that when embed is true, Mirage basically ignores the serializeIds option, since it thinks your resources will always be embedded. (The idea here is that a foreign key is used to fetch related resources separately, but when they're embedded they always come over together.) This is also confusing and doesn't need to be the case. I've opened a tracking issue in Mirage to help address these points.
As for you today, the best way to solve this is to leave root to true and embed false, which are both the defaults, so that serializeIds works properly, and then just write your own serialize() function to remove the key for you:
const ApplicationSerializer = Serializer.extend({
// will always serialize the ids of all relationships for the model or collection in the response
serializeIds: "always",
serialize(resource, request) {
let json = Serializer.prototype.serialize.apply(this, arguments);
let root = resource.models ? this.keyForCollection(resource.modelName) : this.keyForModel(resource.modelName)
return json[root];
}
});
You should be able to test this out on both /invoices and /invoices/1.
Check out this REPL example and try making a request to each URL.
Here's the config from the example:
import {
Server,
Serializer,
Model,
belongsTo,
hasMany,
Factory,
} from "miragejs";
import faker from "faker";
const ApplicationSerializer = Serializer.extend({
// will always serialize the ids of all relationships for the model or collection in the response
serializeIds: "always",
serialize(resource, request) {
let json = Serializer.prototype.serialize.apply(this, arguments);
let root = resource.models ? this.keyForCollection(resource.modelName) : this.keyForModel(resource.modelName)
return json[root];
}
});
export default new Server({
models: {
invoice: Model.extend({
customer: belongsTo(),
}),
customer: Model.extend({
invoices: hasMany(),
}),
},
factories: {
invoice: Factory.extend({
title(i) {
return "Invoice " + i;
},
afterCreate(invoice, server) {
if (!invoice.customer) {
invoice.update({
customer: server.create("customer"),
});
}
},
}),
customer: Factory.extend({
name() {
return faker.name.firstName() + " " + faker.name.lastName();
},
}),
},
seeds(server) {
server.createList("invoice", 10);
},
serializers: {
application: ApplicationSerializer,
},
routes() {
this.resource("invoice");
},
});
Hopefully that clears things up + sorry for the confusing APIs!
Kind of a niche question, but I know what the issue is so hopefully someone here can help me out. This is an Observable/RXFire issue, not an xstate issue.
I have this machine that invokes an observable:
export const tribeMachine = Machine(
{
id: "council",
initial: "init",
context: {},
states: {
init: {
invoke: {
id: "gettribes",
src: () =>
collectionData(database.collection("tribes")).pipe(
concatAll(),
map(x => ({ type: "STORE", x }))
),
onDone: "loaded"
},
on: {
STORE: {
actions: "storetribes"
},
CANCEL: "loaded"
}
},
loaded: {
entry: () => console.log("loaded")
},
error: {
entry: () => console.log("error")
}
}
},
{
actions: {
storetribes: (context, event) => console.log("hello")
}
}
);
The way it's supposed to work is that the machine invokes the observable on load, and then once the obs is done emitting its values and calls complete(), invoke.onDone is called and the machine transitions to the 'loaded' state.
When I use a normal observable that i created with a complete() call, or when i add take(#) to the end of my .pipe(), the transition works.
But for some reason the observable that comes from collectionData() from RXFire doesn't send out a 'complete' signal... and the machine just sits there.
I've tried adding a empty() to the end and concat()-ing the observables to add a complete signal to the end of the pipe... but then I found out that empty() is deprecated and it didn't seem to work anyway.
Been banging my head against the wall for awhile. any help is appreciated.
Edit:
Solution:
I misunderstood the purpose of collectionData(). It is a listener, so it's not supposed to complete. I was putting a square peg in round hole. The solution is to refactor the xstate machine so I don't need to call onDone at all.
Thank you for the answers nonetheless.
EDIT2: GOT IT TO WORK.
take(1) can be called BEFORE concatAll(). I thought if you called it first it would end the stream, but it doesn't. The rest of the operators in the pipe still apply. So i take(1) to get the single array, use concatAll() to flatten the array into a stream of individual objects, then map that data to a new object which triggers the STORE action. the store action then sets the data to the context of the machine.
export const tribeMachine = Machine({
id: 'council',
initial: 'init',
context: {
tribes: {},
markers: []
},
states: {
init: {
invoke: {
id: 'gettribes',
src: () => collectionData(database.collection('tribes')).pipe(
take(1),
concatAll(),
map(value => ({ type: 'TRIBESTORE', value })),
),
onDone: 'loaded'
},
on: {
TRIBESTORE: {
actions: ['storetribes', 'logtribes']
},
CANCEL: 'loaded'
}
},
loaded: {
},
error: {
}
}
},
{
actions: {
storetribes: assign((context, event) => {
return {
tribes: {
...context.tribes,
[event.value.id]: event.value
},
markers: [
...context.markers,
{
lat: event.value.lat,
lng: event.value.lng,
title: event.value.tribeName
}
]
}
})
}
}
)
Thanks for everyone's help!
Observables can return multiple values over time, so it is up to collectionData() to decide when to finish (i.e. causing complete() to be called).
However, if you only want to take 1 value from the observable, you can try:
collectionData(database.collection("tribes")).pipe(
take(1),
concatAll(),
map(x => ({ type: "STORE", x }))
),
This will cause the observable to complete once you take 1 value from collectionData().
Note: This may not be the best solution as it depends on how the observable streams you are using works. I am just highlighting that you can use take(1) to just take 1 value and complete the source observable.
As far as my understanding goes, it's an anti-pattern to dispatch actions from within a store update handler. Correct?
How can I handle the following workflow then?
I have some company switcher on my page header
Clicking on a company dispatches some SELECTEDCOMPANY_UPDATE action
The active view reacts on the according change in the state store by forcing a data reload. E.g. by calling companyDataService.fetchOrders(companyName).
I'd like to show some loading animation during the data is being fetched and therefore have an dedicated action like FETCHINGDATA_UPDATE which updates the fetchingData section in my app state store to which all interested views can react by showing/hiding the load mask
Where do I actually dispatch the FETCHINGDATA_UPDATE action? If I directly do this from within companyDataService.fetchOrders(companyName) it would be called from within a store update handler (see OrdersView.onStoreUpdate in exemplary code below)...
Edit
To clarify my last sentence I'm adding some exemplary code which shows how my implementation would have looked like:
ActionCreator.js
// ...
export function setSelectedCompany(company) {
return { type: SELECTEDCOMPANY_UPDATE, company: company };
}
export function setFetchingData(isFetching) {
return { type: FETCHINGDATA_UPDATE, isFetching: isFetching };
}
// ...
CompanyDataService.js
// ...
export fetchOrders(companyName) {
this.stateStore.dispatch(actionCreator.setFetchingData(true));
fetchData(companyName)
.then((data) => {
this.stateStore.dispatch(actionCreator.setFetchingData(false));
// Apply the data...
})
.catch((err) => {
this.stateStore.dispatch(actionCreator.setFetchingData(false));
this.stateStore.dispatch(actionCreator.setFetchError(err));
})
}
// ...
CompanySwitcher.js
// ...
onCompanyClicked(company) {
this.stateStore.dispatch(actionCreator.setSelectedCompany(company));
}
// ...
OrdersView.js
// ...
constructor() {
this._curCompany = '';
this.stateStore.subscribe(this.onStoreUpdate);
}
// ...
onStoreUpdate() {
const stateCompany = this.stateStore.getState().company;
if (this._curCompany !== stateCompany) {
// We're inside a store update handler and `fetchOrders` dispatches another state change which is considered bad...
companyDataService.fetchOrders(stateCompany);
this._curCompany = stateComapny;
}
}
// ...
I agree with Davin, in the action creator is the place to do this, something like:
export function fetchOrders (company) {
return (dispatch) => {
dispatch ({ type: FETCHINGDATA_UPDATE });
return fetchOrderFunction ().then(
(result) => dispatch ({ type: FETCHING_COMPLETED, result }),
(error) => dispatch ({ type: FETCHING_FAILED, error })
);
};
}
Then in the reducer FETCHINGDATA_UPDATE can set your loading indicator to true and you can set it back to false I both SUCCESS and FAILED
Yo! I'm using Redux and Normalizr. The API I'm working with sends down objects that look like this:
{
name: 'Foo',
type: 'ABCD-EFGH-IJKL-MNOP'
}
or like this
{
name: 'Foo2',
children: [
'ABCD-EFGH-IJKL-MNOP',
'QRST-UVWX-YZAB-CDEF'
]
}
I want to be able to asynchronously fetch those related entities (type and children) when the above objects are accessed from the state (in mapStateToProps). Unfortunately, this does not seem to mesh with the Redux way as mapStateToProps is not the right place to call actions. Is there an obvious solution to this case that I'm overlooking (other than pre-fetching all of my data)?
Not sure that I have correctly understood your use-case, but if you want to fetch data, one simple common way is to trigger it from a React component:
var Component = React.createClass({
componentDidMount: function() {
if (!this.props.myObject) {
dispatch(actions.loadObject(this.props.myObjectId));
}
},
render: function() {
const heading = this.props.myObject ?
'My object name is ' + this.props.myObject.name
: 'No object loaded';
return (
<div>
{heading}
</div>
);
},
});
Given the "myObjectId" prop, the component triggers the "myObject" fetching after mounting.
Another common way would be to fetch the data, if it's not already here, from a Redux async action creator (see Redux's doc for more details about this pattern):
// sync action creator:
const FETCH_OBJECT_SUCCESS = 'FETCH_OBJECT_SUCCESS';
function fetchObjectSuccess(objectId, myObject) {
return {
type: FETCH_OBJECT_SUCCESS,
objectId,
myObject,
};
}
// async action creator:
function fetchObject(objectId) {
return (dispatch, getState) => {
const currentAppState = getState();
if (!currentAppState.allObjects[objectId]) {
// fetches the object if not already present in app state:
return fetch('some_url_.../' + objectId)
.then(myObject => (
dispatch(fetchObjectSuccess(objectId, myObject))
));
} else {
return Promise.resolve(); // nothing to wait for
}
};
}