I'm using the Flux design pattern in my application and I need to test that an Application Action emits on a given Store in the payload that a component sends.
Using a Dispatcher I send a payload like the following:
dispatcher.dispatch({
action: ACTION_NAME,
foo: 'bar',
emitOn: {
store: SomeStore, // extends EventEmitter
componentIds: ['Foo-Bar']
}
}
The dispatcher is implemented like so (Not necessarily important)
Dispatcher.register((payload) => {
Actions.call(payload);
return true;
});
When the dispatcher calls an action the object Actions will call that action and when the action finishes it should call emit on the given store.
My question is how do I test this in my application? I want to know if it's possible to check that emit was called after an action finishes.
To finish an action this function is called Actions.finish(payload)
And if you're curious what finish looks like:
finish(payload) {
payload.emitOn.map(emitter => {
var store = emitter.store;
emitter.componentIds.map(id => {
store.emit(id);
});
});
}
My current testing code, but the Error is never thrown:
jest.mock('./TestStore.js', () => {
return function() {
return {
emit: () => {
throw new Error('Test Error');
}
};
};
});
let ACTION = 'Test-Action';
let payload = {
action: ACTION,
emitOn: [{
store: TestStore, // The store to emit on
componentIds: ['Test-Id']
}]
};
expect(() => {
AppActions.finish(payload);
}).toThrow(Error);
SO for anyone caring about how to test if a function is called you can spy on a function using jest.spyOn
I found this here Understanding Jest Mocks
I changed my testing code (Much cleaner and the right approach to this kind of testing)
const emitMock = jest.spyOn(TestStore, 'emit');
let ACTION = 'Test-Action';
let payload = {
action: ACTION,
emitOn: [{
store: TestStore, // The store to emit on
componentIds: ['Test-Id']
}]
};
AppActions.finish(payload);
expect(emitMock).toHaveBeenCalledTimes(1);
Related
As the tittle says, I would like to be able to use the same axios response for differents components.
I have some restrictions like, I'm onlyl able to use react by adding scripts tags to my html so things like exports or jsx are impossible for me.
This is my react code:
class User extends React.Component {
state = {
user: {}
}
componentWillMount() {
console.log(localStorage.getItem("user"))
axios.get('http://localhost:8080/dashboard?user=' + localStorage.getItem("user"))
.then(res => {
const userResponse = res.data
setTimeout(() =>
this.setState({user: userResponse.user}), 1000);
})
}
render () {
const {user} = this.state
if (user.fullName === undefined)
return React.createElement("div", null, 'loading..');
return React.createElement("span", {className: "mr-2 d-none d-lg-inline text-gray-600 small" }, user.fullName);
}
}
ReactDOM.render( React.createElement(User, {}, null), document.getElementById('userDropdown') );
class Roles extends React.Component{
state = {
user: {}
}
componentWillMount() {
console.log(localStorage.getItem("user"))
axios.get('http://localhost:8080/dashboard?user=' + localStorage.getItem("user"))
.then(res => {
const userResponse = res.data
setTimeout(() =>
this.setState({user: userResponse.user}), 1000);
})
}
render () {
const {user} = this.state
const roles = user.user.roles.map((rol) => rol.roleName)
if (user.fullName === undefined)
return React.createElement("div", null, 'loading..');
return React.createElement("a", {className: "dropdown-item" }, user.fullName);
}
}
ReactDOM.render( React.createElement(Roles, {}, null), document.getElementById('dropdownRol') );
I would like to be able to manage different components(rendering each one) with data of the same axios response.
Is this possible considering my limitations?
Thanks in advance
Here's a working example of how you might do it. I've tried to annotate everything with comments, but I'm happy to try to clarify if you have questions.
// Fake response object for the store's "load" request
const fakeResponse = {
user: {
fullName: "Carolina Ponce",
roles: [
{ roleName: "administrator" },
{ roleName: "editor" },
{ roleName: "moderator" },
{ roleName: "generally awesome person" }
]
}
};
// this class is responsible for loading the data
// and making it available to other components.
// we'll create a singleton for this example, but
// it might make sense to have more than one instance
// for other use cases.
class UserStore {
constructor() {
// kick off the data load upon instantiation
this.load();
}
// statically available singleton instance.
// not accessed outside the UserStore class itself
static instance = new this();
// UserStore.connect creates a higher-order component
// that provides a 'store' prop and automatically updates
// the connected component when the store changes. in this
// example the only change occurs when the data loads, but
// it could be extended for other uses.
static connect = function(Component) {
// get the UserStore instance to pass as a prop
const store = this.instance;
// return a new higher-order component that wraps the connected one.
return class Connected extends React.Component {
// when the store changes just force a re-render of the component
onStoreChange = () => this.forceUpdate();
// listen for store changes on mount
componentWillMount = () => store.listen(this.onStoreChange);
// stop listening for store changes when we unmount
componentWillUnmount = () => store.unlisten(this.onStoreChange);
render() {
// render the connected component with an additional 'store' prop
return React.createElement(Component, { store });
}
};
};
// The following listen, unlisten, and onChange methods would
// normally be achieved by having UserStore extend EventEmitter
// instead of re-inventing it, but I wasn't sure whether EventEmitter
// would be available to you given your build restrictions.
// Adds a listener function to be invoked when the store changes.
// Called by componentWillMount for connected components so they
// get updated when data loads, etc.
// The store just keeps a simple array of listener functions. This
// method creates the array if it doesn't already exist, and
// adds the new function (fn) to the array.
listen = fn => (this.listeners = [...(this.listeners || []), fn]);
// Remove a listener; the inverse of listen.
// Invoked by componentWillUnmount to disconnect from the store and
// stop receiving change notifications. We don't want to attempt to
// update unmounted components.
unlisten = fn => {
// get this.listeners
const { listeners = [] } = this;
// delete the specified function from the array.
// array.splice modifies the original array so we don't
// need to reassign it to this.listeners or anything.
listeners.splice(listeners.indexOf(fn), 1);
};
// Invoke all the listener functions when the store changes.
// (onChange is invoked by the load method below)
onChange = () => (this.listeners || []).forEach(fn => fn());
// do whatever data loading you need to do here, then
// invoke this.onChange to update connected components.
async load() {
// the loading and loaded fields aren't used by the connected
// components in this example. just including them as food
// for thought. components could rely on these explicit fields
// for store status instead of pivoting on the presence of the
// data.user object, which is what the User and Role components
// are doing (below) in this example.
this.loaded = false;
this.loading = true;
try {
// faking the data request. wait two seconds and return our
// hard-coded data from above.
// (Replace this with your network fetch.)
this.data = await new Promise(fulfill =>
setTimeout(() => fulfill(fakeResponse), 2000)
);
// update the loading/loaded status fields
this.loaded = true;
this.loading = false;
// call onChange to trigger component updates.
this.onChange();
} catch (e) {
// If something blows up during the network request,
// make the error available to connected components
// as store.error so they can display an error message
// or a retry button or whatever.
this.error = e;
}
}
}
// With all the loading logic in the store, we can
// use a much simpler function component to render
// the user's name.
// (This component gets connected to the store in the
// React.createElement call below.)
function User({ store }) {
const { data: { user } = {} } = store || {};
return React.createElement(
"span",
{ className: "mr-2 d-none d-lg-inline text-gray-600 small" },
user ? user.fullName : "loading (User)…"
);
}
ReactDOM.render(
// Connect the User component to the store via UserStore.connect(User)
React.createElement(UserStore.connect(User), {}, null),
document.getElementById("userDropdown")
);
// Again, with all the data loading in the store, we can
// use a much simpler functional component to render the
// roles. (You may still need a class if you need it to do
// other stuff, but this is all we need for this example.)
function Roles({ store }) {
// get the info from the store prop
const { data: { user } = {}, loaded, loading, error } = store || {};
// handle store errors
if (error) {
return React.createElement("div", null, "oh noes!");
}
// store not loaded yet?
if (!loaded || loading) {
return React.createElement("div", null, "loading (Roles)…");
}
// if we made it this far, we have user data. do your thing.
const roles = user.roles.map(rol => rol.roleName);
return React.createElement(
"a",
{ className: "dropdown-item" },
roles.join(", ")
);
}
ReactDOM.render(
// connect the Roles component to the store like before
React.createElement(UserStore.connect(Roles), {}, null),
document.getElementById("dropdownRol")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="userDropdown"></div>
<div id="dropdownRol"></div>
I have a vuejs component and a vuex store.
I would like to send data from vue component to vuejs store and then call a function in vuex that's push data to a db.
I get the data from currentUser (that works), but in vuex store I get the error: Cannot read property 'push' of null.
I run createPost that works but the data does not pushed to vuex store I think because the error above.
#vuejs component
import { mapState, mapGetters, mapMutations, mapActions } from "vuex";
import {
SET_NEWPOST,
ADD_TAGS,
SET_USERDATA,
SET_GENERAL
} from "#/store/posts/mutations";
methods: {
...mapMutations("posts", {
updateInformation: SET_NEWPOST,
setUserData: SET_USERDATA,
addGeneral: SET_GENERAL,
addTags: ADD_TAGS
}),
...mapActions("posts", {
create: "triggerAddProductAction"
}),
async createPost() {
this.updateInformation({
content: this.content,
url: this.newOne
});
this.updateUserData();
this.createOne();
}
}
vuex store
...
const state = {
products: []
}
const mutations = {
[addProduct]: (state, product) => state.products.push(product)
},
const actions: {
createUserProduct: async ({ commit, rootState }, product) => {
const userProductDb = new UserProductsDB(
rootState.authentication.user.id
);
const createdProduct = await userProductDb.create(product);
commit("addProduct", createdProduct);
},
triggerAddProductAction: ({ dispatch, state, commit }) => {
const post = state.newPost;
dispatch("createUserProduct", post);
}
}
Your format I believe is a little off. Try building the store like this. Remember that using arrow functions vs non-arrow functions can also have a side effect in what is being referenced.
Mostly what can be seen, is that I removed the const's, and placed it all in the object literal directly. I also remove the Destructuring of addProduct as it doesn't seem logical here.
const store = new Vuex.Store({
state: {
products: []
},
mutations: {
addProduct: (state, product) => {
state.products.push(product)
console.log('Added Product:', product)
console.log('products', state.products)
}
},
actions: {
async createUserProduct({ commit }, product) {
commit("addProduct", product);
}
}
});
new Vue({
el: "#app",
store,
mounted() {
this.$store.dispatch('createUserProduct', 1)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.1.0/vuex.min.js"></script>
<div id="app"></div>
I think one of the main problems here is actually that you call mutations directly in your component. Mutations should always be called by actions and not directly. This is because mutations are synchronous and actions can be asynchronous. From Vuex docs:
On to Actions
Asynchronicity combined with state mutation can make your program very hard to reason about. For example, when you call two methods both with async callbacks that mutate the state, how do you know when they are called and which callback was called first? This is exactly why we want to separate the two concepts. In Vuex, mutations are synchronous transactions:
store.commit('increment')
// any state change that the "increment" mutation may cause
// should be done at this moment.
To handle asynchronous operations, let's introduce Actions.
That's why you should have a structure like this:
export const mutations = {
ADD_EVENT(state, event) {
state.events.push(event)
},
SET_EVENTS(state, events) {
state.events = events
},
SET_EVENTS_TOTAL(state, eventsTotal) {
state.eventsTotal = eventsTotal
},
SET_EVENT(state, event) {
state.event = event
}
}
export const actions = {
createEvent({ commit, dispatch }, event) {
return EventService.postEvent(event)
.then(() => {
commit('ADD_EVENT', event)
commit('SET_EVENT', event)
const notification = {
type: 'success',
message: 'Your event has been created!'
}
dispatch('notification/add', notification, { root: true })
})
.catch(error => {
const notification = {
type: 'error',
message: 'There was a problem creating your event: ' + error.message
}
dispatch('notification/add', notification, { root: true })
throw error
})
}
Check also this video out by vuemastery even featured on the official vuex docs: https://www.vuemastery.com/courses/mastering-vuex/intro-to-vuex/
My problem was that I am trying to make a unit test for a function but can't figure out how to test a part of it.
This is a react / redux action that does the following:
1) retrieves json data with an image url
2) loads the image into an Image instance and dispatches its size to the reducer (asynchronously when image is loaded using Image.onload)
3) dispatches that the fetch was completed to the reducer
The image onload happens asynchronously, so when I try to unit test it it wouldn't be called. Moreover, I can't just mock things out because the image instance is created within the function...
Here's the code I wanted to test (removing some checks, branching logic, and stuff):
export function fetchInsuranceCardPhoto() {
return dispatch => {
dispatch(requestingInsuranceCardPhoto());
return fetch(`${api}`,
{
headers: {},
credentials: 'same-origin',
method: 'GET',
})
.then(response => {
switch (response.status) {
case 200:
return response.json()
.then(json => {
dispatch(receivedInsuranceCardPhoto(json));
})
}
});
};
}
function receivedInsuranceCardPhoto(json) {
return dispatch => {
const insuranceCardFrontImg = json.insuranceCardData.url_front;
const insuranceCardBackImg = json.insuranceCardData.url_back;
if (insuranceCardFrontImg) {
dispatch(storeImageSize(insuranceCardFrontImg, 'insuranceCardFront'));
}
return dispatch(receivedInsuranceCardPhotoSuccess(json));
};
}
function receivedInsuranceCardPhotoSuccess(json) {
const insuranceCardFrontImg = json.insuranceCardData.url_front;
const insuranceCardBackImg = json.insuranceCardData.url_back;
const insuranceCardId = json.insuranceCardData.id;
return {
type: RECEIVED_INSURANCE_CARD_PHOTO,
insuranceCardFrontImg,
insuranceCardBackImg,
insuranceCardId,
};
}
function storeImageSize(imgSrc, side) {
return dispatch => {
const img = new Image();
img.src = imgSrc;
img.onload = () => {
return dispatch({
type: STORE_CARD_IMAGE_SIZE,
side,
width: img.naturalWidth,
height: img.naturalHeight,
});
};
};
}
Notice in that last storeImageSize private function how there's an instance of Image created and an image.onload that is assigned to a function.
Now here's my test:
it('triggers RECEIVED_INSURANCE_CARD_PHOTO when 200 returned without data', async () => {
givenAPICallSucceedsWithData();
await store.dispatch(fetchInsuranceCardPhoto());
expectActionsToHaveBeenTriggered(
REQUESTING_INSURANCE_CARD_PHOTO,
RECEIVED_INSURANCE_CARD_PHOTO,
STORE_CARD_IMAGE_SIZE,
);
});
This test though will fail because the test finishes before the image.onload callback is called.
How can I force the image.onload callback to be called so that I can test that the `STORE_CARD_IMAGE_SIZE action gets broadcasted?
After some investigation, I found a very interesting javascript function that would solve my issue.
It is this: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
Here's how I used Object.defineProperty(...) to solve my issue:
describe('fetchInsuranceCardPhoto', () => {
let imageOnload = null;
/** Override Image global to save onload setting here so that I can trigger it manually in my test */
function trackImageOnload() {
Object.defineProperty(Image.prototype, 'onload', {
get: function () {
return this._onload;
},
set: function (fn) {
imageOnload = fn;
this._onload = fn;
},
});
}
it('triggers RECEIVED_INSURANCE_CARD_PHOTO when 200 returned with data', async () => {
trackImageOnload();
givenAPICallSucceedsWithData();
await store.dispatch(fetchInsuranceCardPhoto());
imageOnload();
expectActionsToHaveBeenTriggered(
REQUESTING_INSURANCE_CARD_PHOTO,
RECEIVED_INSURANCE_CARD_PHOTO,
STORE_CARD_IMAGE_SIZE,
);
});
What I did here was use define property to override the setter of any instance of Image. the setter would continue to get or set like normal but would also save the value (in this case a function) that was set to a variable in the scope of the unit test. After which, you can just run that function you captured before the verification step of your the test.
Gotchas
- configurable needs to be set
- note that defineProperty is a different function than defineProperties
- This is bad practice in real code.
- remember to use the prototype
Hope this post can help a dev in need!
I just implemented a drag and drop feature with react-dnd and when the user drops the SkyElement item in my app, I update top and left on the server which in turn updates the redux store
However, the update call works occasionally, not every time. And in my console, I see a warning; updateElementSaga has been cancelled
In my SlotView.js, in a function, I have:
this.props.dispatch(requestUpdateElement({ id, top, left }));
In my elements/actions.js:
export function requestUpdateElement(element) {
return { type: 'requestUpdateElement', element };
}
In my elements/sagas.js:
export function *updateElementSaga(action) {
const response = yield call(api.updateElement, action.element);
if (response.element) {
// debugger; // this hits, saga was cancelled will have appeared in the console at this point
yield put(actions.receiveElement(response.element));
} else if (response.error) {
console.log('error receiving element');
}
}
export default [
takeLatest('requestUpdateElement', updateElementSaga),
];
In api.js:
export function updateElement(element) {
const userId = JSON.parse(localStorage.cookies).userId;
element.userId = userId;
if (userId) {
return apiHelper.put(
`${apiHelper.getBaseUrl()}/users/${element.userId}/elements/${element.id}`,
{element},
{headers: apiHelper.getHeaders()}
).catch((error) => {
return {error};
});
} else {
console.log('user ID could not be found for request');
}
}
And my elements/reducer.js:
const defaultState = {
elementsMap: {},
visibleElements: [],
unplacedElements: [],
};
export default function(state = defaultState, action) {
switch (action.type) {
case 'receiveElement':
let element = null;
let unplacedElement = null;
if (action.element.sectorId === undefined) {
unplacedElement = `${action.element.id}`;
} else {
element = `${action.element.id}`;
// don't add, duplicate
const newState = {...state}; // copy old state
delete newState[`${action.element.id}`]; // delete the item from the object
const newVisibleElements = newState.visibleElements.filter(e => e !== `${action.element.id}`); // remove item from visible elements
const newUnplacedElements = newState.unplacedElements.filter(e => e !== `${action.element.id}`);
return {
...newState,
elementsMap: {
...newState.elementsMap,
[element]: action.element,
},
visibleElements: [...newVisibleElements, element],
unplacedElements: [...newUnplacedElements],
};
}
return {
...state,
elementsMap: {
...state.elementsMap,
[action.element.id]: action.element,
},
visibleElements: [...state.visibleElements, element],
unplacedElements: [...state.unplacedElements, unplacedElement],
};
default:
return state;
}
}
Like I mentioned before, sometimes the update works, but not every time. I've narrowed the problem down to the client. Server seems to be acting fine. Any idea what I'm doing wrong here? Thanks!
If you are using takeLatest the redux saga documentation does mention:
https://redux-saga.js.org/docs/basics/UsingSagaHelpers.html
Unlike takeEvery, takeLatest allows only one fetchData task to run at
any moment. And it will be the latest started task. If a previous
task is still running when another fetchData task is started, the
previous task will be automatically cancelled.
Where fetchData is the generator function that is being served using takeLatest or takeEvery
And when your UI keeps invoking the same action, before it gets completed, it will keep cancelling
the last invoked action, and hence you would keep getting the message intermittently:
updateElementSaga has been cancelled
Which by nature takeLatest is doing the right thing. Which is:
Always take the latest invoked action
In case you want every action to be caught and processed, do use takeEvery, as:
export default [
takeEvery('requestUpdateElement', updateElementSaga),
];
I am new in ReactJS and "reactive programming". I tried to create a dispatcher, action and store according to this project, but I don't know how to pass data to component.
In this example it doesn't work.
var data = [1, 2, 3, 4, 5];
var AppDispatcher = Kefir.emitter();
function DataActions() {
this.getAllData = function () {
AppDispatcher.emit({
actionType: "GET_ALL"
});
};
}
var Actions = new DataActions();
var getAllDataActionsStream = AppDispatcher.filter(function (action) {
return action.actionType === "GET_ALL";
}).map(function (action) {
return function (data) {
return data;
};
});
var dataStream = Kefir.merge([getAllDataActionsStream]).scan(function (prevData, modificationFunc) {
return modificationFunc(prevData);
}, {});
var Content = React.createClass({
getInitialState: function() {
this.onDataChange = this.onDataChange.bind(this);
return {componentData: []};
},
componentDidMount: function() {
dataStream.onValue(this.onDataChange);
},
componentWillMount: function(){
dataStream.offValue(this.onDataChange);
console.log(Actions.getAllData());
},
onDataChange(newData) {
this.setState({componentData: newData});
},
render: function() {
console.log(this.state);
var list = this.state.componentData.map(function (item, i) {
return (
<li key={i}>{item}</li>
);
});
return <ul>{list}</ul>;
}
});
React.render(<Content />, document.getElementById('container'));
Before I begin to answer in length I want to answer this part up front:
but I don't know how to pass data to component.
In the example you linked the author passes in the Todos into the main component using React's props, not with an action. So that is the approach I take in my example as well.
Now here is my example. I highly reccommend looking at the example and reading along to what I've written below.
var data = [ 1, 2, 3, 4, 5 ];
// This will now log all events of the AppDispatcher in the console with the prefix 'Kefer: '
var AppDispatcher = Kefir.emitter().log("Kefir: ");
function DataActions() {
// Our application has an action of emitting a random number.
this.emitNumber = function() {
AppDispatcher.emit({
actionType: "EMIT_NUMBER"
})
};
}
var Actions = new DataActions();
var emitNumberActionStream = AppDispatcher
.filter(function(action) {
return action.actionType === "EMIT_NUMBER";
})
.map(function(action) {
console.log("EMIT_NUMBER ACTION OCCURRED!!");
return Math.floor(Math.random() * (10)) + 1;
});
// Only one stream, no need to merge right now.
//var dataStream = Kefir.merge([ getAllDataActionsStream ]);
var Content = React.createClass({
getInitialState: function() {
// Set initial componentData using the data passed into this component's via props
return { componentData: this.props.data };
},
componentDidMount: function() {
// On each emitted value run the this.onDataChange function
emitNumberActionStream.onValue(this.onDataChange);
// Every second emit a number using the Actions we created earlier
setInterval(function() {
Actions.emitNumber();
}, 1000);
},
onDataChange: function(emittedNumber) {
console.log('state on change:', this.state);
// Update the state by appending the emitted number to the current state's componentData
this.setState({ componentData: this.state.componentData.concat([emittedNumber])});
console.log('updated state: ', this.state);
console.log('-----------------');
},
render: function() {
console.log('RENDER AGAIN!');
var list = this.state.componentData.map(function(item, i) {
return (
<li key={i}>{item}</li>
);
});
return <ul>{list}</ul>;
}
})
;
// Pass in initial data using props 'data={data}'
React.render(<Content data={data}/>, document.getElementById('container'));
I modified the example you gave that wasn't working so that it works and makes a little more sense (hopefully).
The Actions and Stores work like this:
Actions:
Request a number be emitted
Stores
Listen for "EMIT_NUMBER" actions and emit a random number
And the actual component runs like this:
It gets the initial 5 numbers passed into the component via props.
Once mounted it begins listening to the store and creates a setInterval that calls the action dispatcher's emitNumber() action. The interval is to show the reactivity at work, you could imagine that there was a button to press that would call emitNumber() instead.
The store observes the action dispatcher emit "EMIT_NUMBER" and emits a number.
The component observes the store emitted a number and updates the component's state.
The component observes that its state has changed and it rerenders.
I believe the issue is that you're using ES6 syntax (which is what the example was written in... notice the Readme). You'll need to either use a transpiler like Babel or convert your method(param => console.log(param)) syntax into normal JS (ie, method(function(param) { console.log(param) });).