I am building a react-redux app, using custom redux middleware.
In the definition of my project, action only provides an object to define action type and necessary parameters for middleware and reducer. All the ajax request will be handle by middleware. This is the life cycle would look like:
action -> middleware(if action is intercepted) -> reducer -> store
When the user tries to log in, the operation on the react component will fire an action, which would look like this:
export function login(username, password) {
return {
type: 'LOGIN',
username: username,
password: password
}
}
export function authSucceed(username, isAdmin) {
return {
type: 'AUTHSUCCEED',
username: username,
isAdmin: isAdmin
}
}
export function authFail(text) {
return {
type: 'AUTHFAIL',
errorMessage: text
}
}
Then middleware will use the parameters passed in action to send ajax request, which would be like this.
export function customedMiddleware(store) {
return next => action => {
if (action.type === 'LOGIN') {
axios.post(url + '/api/login', {
username: action.username,
password: action.password
})
.then(res => {
if (res.status === 200) {
store.dispatch(actions.authSucceed(res.data.username, res.data.isAdmin));
} else {
store.dispatch(actions.authFail(res.data));
}
})
.catch(error => console.log(error));
}
return next(action);
};
}
After the middleware sends login request to server, depending on whether the authentication succeeds or not, the middleware will dispatch some action in reducer correspondingly. Since authSucceed and authFail would not be intercepted by middleware, reducer will process accordingly.
export default function(state = false, action) {
switch(action.type) {
case 'AUTHSUCCEED':
return true;
case 'AUTHFAIL':
return false;
case 'LOGOUT':
return false;
}
return state;
}
What has been done here in reducer is to change the system state. If the state is true, the front-end will render the information page. If the state is false, the front-end will remain in the login page.
I like system definition this way. Every MVC part is well isolated. However, it's very difficult to test the middleware. Currently, I am testing this way:
it('should dispatch authSucceed if signup with correct info', () => {
nock('http://localhost:8080')
.post('/api/signup', {
username: 'bruce',
password: 'Gx1234'
})
.reply(200, {
username: 'bruce',
isAdmin: false
});
const createStoreWithMiddleware = applyMiddleware(customedMiddleware)(createStore);
const store = createStoreWithMiddleware(reducers);
const dispatch = sinon.spy(store, 'dispatch');
store.dispatch(actions.login('bruce', 'Gx1234'));
setTimeout(() => {
expect(dispatch.calledWith({
type: 'AUTHSUCCEED',
username: 'bruce',
isAdmin: false
})).to.be.true;
}, 100);
});
I dispatch login action. Then spy the whether authSucceed action and authFail action will be called correctly within 100ms. This method works if there is only one test to be run. If there are more then one test running in sequence, they might affect each other. I have to adjust the time delay of the setTimeout to make it work for all cases, which is 10ms.
I don't feel comfortable this way. I can't make sure whether it just work for me or for everybody too, since absolute time is related to hardware.
I would really appreciate if anybody can give me some advice on how to test this custom middleware.
Your code works fine, but you shouldn't need a setTimeout with such a long time since using nock makes the remote resquest response instantly. The problem is promises enqueue microtasks and they only run after a macrotask is finished (in your case, it()), in the same event loop.
That's why you need setTimeout to enqueue another macrotask, the time doesn't make a difference. I believe setImmediate should work as well.
Related
I am not using redux-thunk. this keeps error-ing and I am not sure how to fix it. The examples I see online use redux-thunk which I am not using
my repo is here and the file I am trying to test is in tests\actions\...
My action that is being called in the test
import axios from "axios";
var CancelToken = axios.CancelToken;
let fetch_cancel;
export const FETCH_CATEGORIES = "fetch_categories";
export async function fetchCategories() {
fetch_cancel && fetch_cancel();
const request = await axios.get(
`https://d1i9eedhsgvpdh.cloudfront.net/production-plentific-static/api-cache/find-a-pro/api/v1/categories/all.json`,
{
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
fetch_cancel = c;
})
}
);
return {
type: FETCH_CATEGORIES,
payload: request
};
}
The error message means that your actions must be plain objects. For example:
store.dispatch({
type: 'ADD_TODO',
text: 'Understand the middleware'
})
If you make an async request, you can't just return an object, because you need to wait for the request to finish. If you return too early, you return a Promise.
However, I cannot reproduce your error in your sandbox.
I am trying to grasp how to return a promise from a Redux Action Creator.
I have read these two articles, but are none the wiser. Maybe I'm just daft. ;)
https://redux.js.org/advanced/middleware
https://medium.com/collaborne-engineering/returning-promises-from-redux-action-creators-3035f34fa74b
My component calls the action called startCreateNote()
The action calls the database API
The database operation finishes and resolves an object {newlyCreated-id, reduxStoreObject}
the Action Creator is dispatched
Now I wish to wait for the Redux Action Creator to resolve before proceeding
Show toaster after Action Creator has resolved
See code below.
//ADD_NOTE action generator
export const addNote = (id, note) => ({
type: ADD_NOTE,
id,
note
})
export const startAddNote = (noteData = {}) => {
return (dispatch, getState) => {
//Database API Instruction object
const apiInstruction = { action: DB_ACTION_ADD, payload: noteData, uid: getState().auth.uid }
//Call database API to insert Note into Database
return noteDatabaseAPI(apiInstruction)
.then(({id, reduxStoreObj}) => {
//The database API resolves id of newly created Note & reduxStore object
//THIS IS WHAT I WANT TO ACHIEVE!
dispatch(addNote(id, reduxStoreObj)).then(
toast("New note saved!")
).catch(
toast.error("ERROR - Note was not saved!")
)
}).catch( (err) => {
//This gets triggered with error message:
//database fail! - TypeError: "dispatch(...).then is not a function"
console.log('database fail! -', err)
})
}
}
How can I make my Action Creator return a promise?
From what I understand the (probably) easiest way to make the action creator return a promise is by using one of several asynchronous-operations libraries such as:
redux-saga
redux-thunk
redux-observable
redux-promise
redux-loop
redux-ship
redux-logic
redux-effects
redux-cycles
redux-side-effects
I found more information here: https://decembersoft.com/posts/what-is-the-right-way-to-do-asynchronous-operations-in-redux/#redux-saga
A clean and easy way to do what you're trying to do is via your reducer.
You could start with adding a isNoteSaved variable in your reducer as follows.
case ADD_NOTE_REQUEST:
return {
...state,
isNoteSaved: false
}
case ADD_NOTE_SUCCESS:
return {
...state,
isNoteSaved: true
}
case ADD_NOTE_FAILURE:
return {
...state,
error: action.payload,
isNoteSaved: false
}
Your isNoteSaved variable has an initial state of false.
Then in place of your promise, you could add something like this :
toast() && addNoteReducer.isNoteSaved
It means your function will be executed when isNoteSaved is true.
That's how I do it. Adding promises to action creators adds complexity and makes the code less readable.
In Cypress, it is well-documented that you can alias specific network requests, which you can then "wait" on. This is especially helpful if you want to do something in Cypress after a specific network request has fired and finished.
Example below from Cypress documentation:
cy.server()
cy.route('POST', '**/users').as('postUser') // ALIASING OCCURS HERE
cy.visit('/users')
cy.get('#first-name').type('Julius{enter}')
cy.wait('#postUser')
However, since I'm using GraphQL in my app, aliasing no longer becomes a straightforward affair. This is because all GraphQL queries share one endpoint /graphql.
Despite it not being possible to differentiate between different graphQL queries using the url endpoint alone, it is possible to differentiate graphQL queries using operationName (refer to following image).
Having dug through the documentation, there doesn't appear to be a way to alias graphQL endpoints using operationName from the request body. I'm also returning the operationName (yellow arrow) as a custom property in my response header; however, I haven't managed to find a way to use it to alias specific graphQL queries either.
FAILED METHOD 1: This method attempts to use the purple arrow shown in image.
cy.server();
cy.route({
method: 'POST',
url: '/graphql',
onResponse(reqObj) {
if (reqObj.request.body.operationName === 'editIpo') {
cy.wrap('editIpo').as('graphqlEditIpo');
}
},
});
cy.wait('#graphqlEditIpo');
This method doesn't work since the graphqlEditIpo alias is registered at runtime and as such, the error I receive is as follows.
CypressError: cy.wait() could not find a registered alias for: '#graphqlEditIpo'. Available aliases are: 'ipoInitial, graphql'.
FAILED METHOD 2: This method attempts to use the yellow arrow shown in image.
cy.server();
cy.route({
method: 'POST',
url: '/graphql',
headers: {
'operation-name': 'editIpo',
},
}).as('graphql');
cy.wait('graphql');
This method doesn't work because the headers property in the options object for cy.route is actually meant to accept response headers for stubbed routes per the docs. Here, I'm trying to use it to identify my specific graphQL query, which obviously won't work.
Which leads me to my question: How can I alias specific graphQL queries/mutations in Cypress? Have I missed something?
The intercept API introduced in 6.0.0 supports this via the request handler function. I used it in my code like so:
cy.intercept('POST', '/graphql', req => {
if (req.body.operationName === 'queryName') {
req.alias = 'queryName';
} else if (req.body.operationName === 'mutationName') {
req.alias = 'mutationName';
} else if (...) {
...
}
});
Where queryName and mutationName are the names of your GQL operations. You can add an additional condition for each request that you would like to alias. You can then wait for them like so:
// Wait on single request
cy.wait('#mutationName');
// Wait on multiple requests.
// Useful if several requests are fired at once, for example on page load.
cy.wait(['#queryName, #mutationName',...]);
The docs have a similar example here: https://docs.cypress.io/api/commands/intercept.html#Aliasing-individual-requests.
This works for me!
Cypress.Commands.add('waitForGraph', operationName => {
const GRAPH_URL = '/api/v2/graph/';
cy.route('POST', GRAPH_URL).as("graphqlRequest");
//This will capture every request
cy.wait('#graphqlRequest').then(({ request }) => {
// If the captured request doesn't match the operation name of your query
// it will wait again for the next one until it gets matched.
if (request.body.operationName !== operationName) {
return cy.waitForGraph(operationName)
}
})
})
Just remember to write your queries with unique names as posible, because the operation name relies on it.
If 'waiting' and not 'aliasing' in itself is the main purpose, the easiest way to do this, as I've encountered thus far, is by aliasing the general graphql requests and then making a recursive function call to 'wait' targeting the newly created alias until you find the specific graphql operation you were looking for.
e.g.
Cypress.Commands.add('waitFor', operationName => {
cy.wait('#graphqlRequest').then(({ request }) => {
if (request.body.operationName !== operationName) {
return cy.waitFor(operationName)
}
})
})
This of course have its caveats and may or may not work in your context. But it works for us.
I hope Cypress enables this in a less hacky way in the future.
PS. I want to give credit to where I got the inspiration to this from, but it seemt to be lost in cyberspace.
Since I was having the same issue and I did not find a real solution for this problem I combined different options and created a workaround that solves my problem. Hopefully this can help someone else too.
I do not really 'wait' for the request to be happen but I catch them all, based on **/graphql url and match the operationName in the request. On a match a function will be executed with the data as parameter. In this function the tests can be defined.
graphQLResponse.js
export const onGraphQLResponse = (resolvers, args) => {
resolvers.forEach((n) => {
const operationName = Object.keys(n).shift();
const nextFn = n[operationName];
if (args.request.body.operationName === operationName) {
handleGraphQLResponse(nextFn)(args.response)(operationName);
}
});
};
const handleGraphQLResponse = (next) => {
return (response) => {
const responseBody = Cypress._.get(response, "body");
return async (alias) => {
await Cypress.Blob.blobToBase64String(responseBody)
.then((blobResponse) => atob(blobResponse))
.then((jsonString) => JSON.parse(jsonString))
.then((jsonResponse) => {
Cypress.log({
name: "wait blob",
displayName: `Wait ${alias}`,
consoleProps: () => {
return jsonResponse.data;
}
}).end();
return jsonResponse.data;
})
.then((data) => {
next(data);
});
};
};
};
In a test file
Bind an array with objects where the key is the operationName and the value is the resolve function.
import { onGraphQLResponse } from "./util/graphQLResponse";
describe("Foo and Bar", function() {
it("Should be able to test GraphQL response data", () => {
cy.server();
cy.route({
method: "POST",
url: "**/graphql",
onResponse: onGraphQLResponse.bind(null, [
{"some operationName": testResponse},
{"some other operationName": testOtherResponse}
])
}).as("graphql");
cy.visit("");
function testResponse(result) {
const foo = result.foo;
expect(foo.label).to.equal("Foo label");
}
function testOtherResponse(result) {
const bar = result.bar;
expect(bar.label).to.equal("Bar label");
}
});
}
Credits
Used the blob command from glebbahmutov.com
This is what you're looking for (New in Cypress 5.6.0):
cy.route2('POST', '/graphql', (req) => {
if (req.body.includes('operationName')) {
req.alias = 'gqlMutation'
}
})
// assert that a matching request has been made
cy.wait('#gqlMutation')
Documentation:
https://docs.cypress.io/api/commands/route2.html#Waiting-on-a-request
I hope that this helps!
I used some of these code examples but had to change it slightly to add the onRequest param to the cy.route and also add the date.Now (could add any auto incrementer, open to other solutions on this) to allow multiple calls to the same GraphQL operation name in the same test. Thanks for pointing me in the right direction!
Cypress.Commands.add('waitForGraph', (operationName) => {
const now = Date.now()
let operationNameFromRequest
cy.route({
method: 'POST',
url: '**graphql',
onRequest: (xhr) => {
operationNameFromRequest = xhr.request.body.operationName
},
}).as(`graphqlRequest${now}`)
//This will capture every request
cy.wait(`#graphqlRequest${now}`).then(({ xhr }) => {
// If the captured request doesn't match the operation name of your query
// it will wait again for the next one until it gets matched.
if (operationNameFromRequest !== operationName) {
return cy.waitForGraph(operationName)
}
})
})
to use:
cy.waitForGraph('QueryAllOrganizations').then((xhr) => { ...
This is how I managed to differentiate each GraphQL request. We use cypress-cucumber-preprocessor so we have a common.js file in /cypress/integration/common/ where we can call a before and beforeEach hook which are called before any feature file.
I tried the solutions here, but couldn't come up with something stable since, in our application, many GraphQL requests are triggered at the same time for some actions.
I ended up storing every GraphQL requests in a global object called graphql_accumulator with a timestamp for each occurence.
It was then easier to manage individual request with cypress command should.
common.js:
beforeEach(() => {
for (const query in graphql_accumulator) {
delete graphql_accumulator[query];
}
cy.server();
cy.route({
method: 'POST',
url: '**/graphql',
onResponse(xhr) {
const queryName = xhr.requestBody.get('query').trim().split(/[({ ]/)[1];
if (!(queryName in graphql_accumulator)) graphql_accumulator[queryName] = [];
graphql_accumulator[queryName].push({timeStamp: nowStamp('HHmmssSS'), data: xhr.responseBody.data})
}
});
});
I have to extract the queryName from the FormData since we don't have (yet) the key operationName in the request header, but this would be where you would use this key.
commands.js
Cypress.Commands.add('waitGraphQL', {prevSubject:false}, (queryName) => {
Cypress.log({
displayName: 'wait gql',
consoleProps() {
return {
'graphQL Accumulator': graphql_accumulator
}
}
});
const timeMark = nowStamp('HHmmssSS');
cy.wrap(graphql_accumulator, {log:false}).should('have.property', queryName)
.and("satisfy", responses => responses.some(response => response['timeStamp'] >= timeMark));
});
It's also important to allow cypress to manage GraphQL requests by adding these settings in /cypress/support/index.js:
Cypress.on('window:before:load', win => {
// unfilters incoming GraphQL requests in cypress so we can see them in the UI
// and track them with cy.server; cy.route
win.fetch = null;
win.Blob = null; // Avoid Blob format for GraphQL responses
});
I use it like this:
cy.waitGraphQL('QueryChannelConfigs');
cy.get(button_edit_market).click();
cy.waitGraphQL will wait for the latest target request, the one that will be stored after the call.
Hope this helps.
Somewhere else this method was suggested.
Btw it all becomes a bit easier once you migrate to Cypress v5.x and make use of the new route (route2) method.
Our use case involved multiple GraphQL calls on one page. We had to use a modified version of the responses from above:
Cypress.Commands.add('createGql', operation => {
cy.route({
method: 'POST',
url: '**/graphql',
}).as(operation);
});
Cypress.Commands.add('waitForGql', (operation, nextOperation) => {
cy.wait(`#${operation}`).then(({ request }) => {
if (request.body.operationName !== operation) {
return cy.waitForGql(operation);
}
cy.route({
method: 'POST',
url: '**/graphql',
}).as(nextOperation || 'gqlRequest');
});
});
The issue is that ALL GraphQL requests share the same URL, so once you create a cy.route() for one GraphQL query, Cypress will match all the following GraphQL queries to that. After it matches, we set cy.route() to just a default label of gqlRequest or the next query.
Our test:
cy.get(someSelector)
.should('be.visible')
.type(someText)
.createGql('gqlOperation1')
.waitForGql('gqlOperation1', 'gqlOperation2') // Create next cy.route() for the next query, or it won't match
.get(someSelector2)
.should('be.visible')
.click();
cy.waitForGql('gqlOperation2')
.get(someSelector3)
.should('be.visible')
.click();
I have a problem with redux trying to load initial data with an asynchronous call to my backend API that returns a JSON. Right now, I'm trying to load a bunch of different articles, but I have no idea how to load it asynchronously. Since it is an initial set of data, should I load it synchronously? If yes, then how would I acheive a synchronous call to my API? If not, how would I go about solving this problem asynchronously?
Right now, I have static json data, data/articles.js that creates a default state in store.js.
Thanks
You should use a redux-thunk middleware, which allows you to dispatch async actions and a fetch library (for example) for downloading your initial data.
So:
1) create an action which fetch your data, example:
export function fetchData() {
const options = {
method: 'GET',
headers: {
'Authorization': 'Client-ID xx' // if theres any needed
}
}
return (dispatch) => {
return fetch('yourUrl.json', options)
.then(response => response.json())
.then(data => dispatch(receiveYourData(data)))
.catch(err => console.log(err));
}
}
receiveYourData is a action which will place your data in your state, example:
export function receiveYourData (payload = []) {
return {
type: RECEIVE_DATA,
payload: payload
}
}
Of course you have to write action handler, which after dispatching an action, will place your data in your state.
If you have your setup (similar to above), you should dispatch fetchData in your componentDidMount lifecycle method (its one of the option of course :) ).
If you dont know how to do particular parts, you can refer to this Example.
Also official async example may be helpful :)
I also had this problem. It turned out that you have to add a lot of code for this simple task. So I simplified this process and created a package for async loading of initial state in redux - redux-async-initial-state.
You can check out examples and in your case in the end your store creator will look like this:
// Load initial state function
const loadStore = () => {
return Promise(resolve => {
fetch('/data/articles.js')
.then(response => response.json())
.then(resolve);
});
}
const storeCreator = applyMiddleware(asyncInitialState.middleware(loadStore));
const store = storeCreator(reducer);
I understand the reason to have the business logic in both client and server, but I don't understand well how to do that in some situations. Here for example:
// client/client.js
// hnadling click event on the Create Accounts button
Template.homecontent.events({
'click #btnCreateAccount': function (event, template) {
var userEmail = template.find('#email').value,
userName = template.find('#newusername').value,
password = template.find('#newpassword').value,
password2 = template.find('#password2').value,
name = template.find('#fullname').value;
validates = true;
//do some validation here
if(password != password2) {
validates = false;
}
if(validates === true) {
Accounts.createUser({
username: userName,
email: userEmail,
password: password,
profile: {
name: name
}
}, function (error) {
if (error) {
console.log("Cannot create user");
}
});
}
}
});
Since the validation is on the client only, it can easily be bypassed.
But there's a problem here: this is triggered by a user event, so I'm not sure what's the best way to have this code running on client & server.
You may be looking for something like Meteor.methods();, which allows you to define functions on the server that the client can call using Meteor.call(). You could provide a validation function and a user save function on the server, and call them both from the client, passing in the form data.
What I have done in the past is (on the client) I have a userFormParse() function that takes a form object and parses it into an object that can be passed into a server side validation function. I use the same userFormParse function for user edit and creation forms.
The validation function returns an error object to the client, or, if it's all valid data, I'll pass the data object on to a userCreateWithRole function (I usually always have roles assigned to users).
On the server:
Meteor.methods({
'createUserWithRole': function(data, role) {
var userId;
Meteor.call('createUserNoRole', data, function(err, result) {
if (err) {
return err;
}
Roles.addUsersToRoles(result, role);
return userId = result;
});
return userId;
},
'createUserNoRole': function(data) {
//Do server side validation
return Accounts.createUser({
email: data.email,
password: data.password,
profile: data.profile
});
}
});
And then on the client:
Template.userSignup.events({
'submit #userSignup': function(event) {
var data, validationErrors;
event.preventDefault();
data = userInputParse($(event.target)); //this function parses form into user object that can be inserted
validationErrors = userObjectValidate(data); //this function takes and does client side validation on the user object.
data.profile.status = 0;
if (validationErrors) {
//Show the user the validation errors
} else {
return Meteor.call('createUserWithRole', data, 'standard', function(err, userId) {
if (!err) {
//User created!!
} else {
//Insertion Error
}
});
}
}
});
That code is conceptual and untested :)
You should be doing it on server side, using Accounts.onCreateUser
The previous answers are not really exact.
Creating and using a Meteor method won't stop users to call the Accounts.createUser from the console for example. Therefore you also need to prevent the creation of users on the client :
Accounts.config({
forbidClientAccountCreation : true
});
You might want to look into Accounts.validateNewUser.
Example (taken from the docs):
Accounts.validateNewUser(function (user) {
if (user.username && user.username.length >= 3)
return true;
throw new Meteor.Error(403, "Username must have at least 3 characters");
});