I am trying to setup tests for some an action creator that is triggering a redux saga.
My saga retrieves a word from a local flask server (will always return the same word) and then displays that word. This is not my real-life case but I tried to start with something easy...
My action creator and saga work as expected when I trigger them with a button in my react app (the word is retrieved from the server, stored in my redux store and the displayed with a selector in my react component), but I cannot get the test to succeed.
I would like to test only the redux part, not the actual rendered react component (not sure if that is part of my problem or not)
I use Enzyme for tests, my store is created correctly and can dispatch the action. I can also see that my saga is being called with the console logs:
My test code:
import { Store } from 'redux';
import { RootState } from '../root.reducer';
import { storeFactory } from '../../../test/testUtils';
import { getSecretWord } from './secret-word.actions';
describe('getSecretWord action creator', () => {
let store: Store<RootState>;
beforeEach(() => {
store = storeFactory();
});
test('add response word to state', () => {
const secretWord = 'party';
store.dispatch(getSecretWord());
const newState = store.getState();
console.log('new state: ' + newState.secretWord);
expect(newState.secretWord).toBe(secretWord);
});
});
and my saga function:
export function* getSecretWordSaga(action: getSecretWordAction): Generator<ForkEffect | CallEffect | PutEffect, void, unknown>
{
try {
console.log('getSecretWordSaga() saga started');
console.log('before axios query call:');
const response:any = yield call(api.get, '/api/word');
// const response = {data: { word: 'party'}, status:200}
console.log('axios query returned: ');
console.log(response);
yield put(setSecretWord(response.data.word));
console.log('getSecretWordSaga() saga finsshed');
} catch (err) {
console.log('error occured:');
console.log(err);
console.log('getSecretWordSaga() saga finsshed with errors');
}
}
export function* getSecretWordSagaStart(): Generator<
ForkEffect<never>,
void,
unknown
> {
yield takeLatest(SecretWordActionTypes.GET_SECRET_WORD, getSecretWordSaga);
}
The axios api is very basic and it includes two interceptors for logging purposes:
import axios from 'axios';
export const api = axios.create({
baseURL: 'http://localhost:5000',
responseType: 'json',
});
api.interceptors.request.use(request => {
console.log('Starting Request', JSON.stringify(request, null, 2))
return request
})
api.interceptors.response.use(response => {
console.log('Response:', JSON.stringify(response, null, 2))
return response
})
I can see in the logs (in "npm test") that I get log for the line "before axios query call:' and one console.log for the request interceptor (everything looks fine there), but no more logs afterwards (neither success nor error)
If I comment out the "yield call.." and hardcode the response (like in the commented out line below), my saga runs through the end and my test succeeds.
Why is the yield Call(api.get, '/api/word') not being executed (and I don't get any error message)?
The code is my opinion correct as it is running fine when executed in react. My flask server is obviously also running and I can see in the flask app than no call to the api are being made by the running tests.
I obviously plan to mock that api call but was also running into some problems there, that's why I first wanted to get the real api call working.
After trying many different ways for adding a timeout, setting the testing function to async and adding a setTimeout in a promise did work.
It's not ideal as I have to set the timeout to a specific value, but I could not figure out a better way to get it working.
test("add response word to state", async () => {
const secretWord = 'party';
store.dispatch(getSecretWord());
await new Promise(res => setTimeout(res, 1000));
const newState = store.getState();
console.log('new state: ' + newState.secretWord);
expect(newState.secretWord).toBe(secretWord);
});
Related
This is a strange one, but here's the situation.
I'm using Next.js with the Next-auth package to handle authentication.
I'm not using Server-Side rendering, it's an admin area, so there is no need for SSR, and in order to authenticate users, I've created a HOC to wrap basically all components except for the "/sign-in" route.
This HOC all does is check if there's a session and then adds the "access token" to the Axios instance in order to use it for all async calls, and if there is no session, it redirects the user to the "sign-in" page like this ...
const AllowAuthenticated = (Component: any) => {
const AuthenticatedComponent = () => {
const { data: session, status }: any = useSession();
const router = useRouter();
useEffect(() => {
if (status !== "loading" && status === "unauthenticated") {
axiosInstance.defaults.headers.common["Authorization"] = null;
signOut({ redirect: false });
router.push("/signin");
} else if (session) {
axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${session.accessToken.accessToken}`;
}
}, [session, status]);
if (status === "loading" || status === "unauthenticated") {
return <LoadingSpinner />;
} else {
return <Component />;
}
};
return AuthenticatedComponent;
};
export default AllowAuthenticated;
And in the Axios instance, I'm checking if the response is "401", then I log out the user and send him to the "sign-in" screen, like this ...
axiosInstance.interceptors.response.use(
response => response,
error => {
const { status } = error.response;
if (status === 401) {
axiosInstance.defaults.headers.common["Authorization"] = null;
signOut({ redirect: false });
return Promise.reject(error);
}
return Promise.reject(error);
},
);
Very simple stuff, and it works like a charm until I decided to upgrade my project to use "react 18.1.0" and "react-dom 18.1.0", then all of a sudden, my API calls doesn't get the "Authorization" header and they return "401" and the user gets logged out :(
If I tried to make an API call inside the HOC right after I set the Auth headers it works, sot I DO get the "token" from the session, but all the async dispatch calls inside the wrapped component return 401.
I forgot to mention, that this issue happens on page refresh, if I didn't refresh the page after I sign in, everything works great, but once I refresh the page the inner async dispatch calls return 401.
I Updated all the packages in my project including Axios and next-auth, but it didn't help.
I eventually had to downgrade back to "react 17.0.2" and everything works again.
Any help is much appreciated.
For those of you who might come across the same issue.
I managed to solve this by not including the logic for adding the token to the "Authorization" header inside the HOC, instead, I used a solution by #kamal-choudhary on a post on Github talking about how to add "JWT" to every axios call using next-auth.
Using #jaketoolson help at that Github post, he was able to attach the token to every "Axios" call.
The solution is basically to create an Axios instance and add an interceptor like I was doing above, but not just for the response, but also for request.
You'll add an interceptor for every request and check if there's a session, and then attach the JWT to the Authorization header.
That managed to solve my issue, and now next-auth works nicely with react 18.
Here's the code he's using ...
import axios from 'axios';
import { getSession } from 'next-auth/react';
const baseURL = process.env.SOME_API_URL || 'http://localhost:1337';
const ApiClient = () => {
const defaultOptions = {
baseURL,
};
const instance = axios.create(defaultOptions);
instance.interceptors.request.use(async (request) => {
const session = await getSession();
if (session) {
request.headers.Authorization = `Bearer ${session.jwt}`;
}
return request;
});
instance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
console.log(`error`, error);
},
);
return instance;
};
export default ApiClient();
Don't forget to give them a thumbs up for their help if it works for you ...
https://github.com/nextauthjs/next-auth/discussions/3550#discussioncomment-1993281
https://github.com/nextauthjs/next-auth/discussions/3550#discussioncomment-1898233
I joined a big/medium project, I am having a hard time creating my first redux-saga-action things, it is going to be a lot of code since they are creating a lot of files to make things readable.
So I call my action in my componentDidMount, the action is being called because I have the alert :
export const fetchDataRequest = () => {
alert("actions data");
return ({
type: FETCH_DATA_REQUEST
})
};
export const fetchDataSuccess = data => ({
type: FETCH_DATA_SUCCESS,
payload: {
data,
},
});
This is my history saga : ( when I call the action with this type, The function get executed )
export default function* dataSaga() {
// their takeEverymethods
yield takeEvery(FETCH_DATA_REQUEST, fetchData);
}
This is what has to be called : ( I am trying to fill my state with data in a json file : mock )
export default function* fetchTronconsOfCircuit() {
try {
// Cal to api
const client = yield call(RedClient);
const data = yield call(client.fetchSomething);
// mock
const history = data === "" ? "" : fakeDataFromMock;
console.log("history : ");
console.log(history);
if (isNilOrEmpty(history)) return null;
yield put(fetchDataSuccess({ data: history }));
} catch (e) {
yield put(addErr(e));
}
}
And this is my root root saga :
export default function* sagas() {
// many other spawn(somethingSaga);
yield spawn(historySaga);
}
and here is the reducer :
const fetchDataSuccess = curry(({ data }, state) => ({
...state,
myData: data,
}));
const HistoryReducer = createSwitchReducer(initialState, [
[FETCH_DATA_SUCCESS, fetchDataSuccess],
]);
The method createSwitchReducer is a method created by the team to create easily a reducer instead of creating a switch and passing the action.type in params etc, their method is working fine, and I did exactly what they do for others.
Am I missing something ?
I feel like I did everything right but the saga is not called, which means it is trivial problem, the connection between action and saga is a common problem I just could not figure where is my problem.
I do not see the console.log message in the console, I added an alert before the try-catch but got nothing too, but alert inside action is being called.
Any help would be really really appreciated.
yield takeEvery(FETCH_DATA_REQUEST, fetchData);
should be
yield takeEvery(FETCH_DATA_REQUEST, fetchTronconsOfCircuit);
I am trying to make my login flow work using socket.io and redux-saga.
There is no problem making the socket connection and validating the user, but when I try to dispatch an action from the "loginSuccess"-event, nothing is happening
The login saga looks like this:
import { takeLatest, call, put, take } from "redux-saga/effects";
import * as types from "../actions/types";
import connect from "../constants/socketConnection";
function* doLogin(action) {
const socket = yield call(connect);
socket.emit("login", {
username: action.username,
password: action.password
});
socket.on("loginSuccess", (response) => {
yield put({type: types.SET_CURRENT_USER, currentUser: response.user})
});
}
export function* login() {
yield takeLatest(types.LOGIN, doLogin);
}
Is it not possible to send an action from within a socket.on()?
The syntax is correct (it works in my other projects), but VS code says that the "put" and "response" are never used and Prettier cannot auto format when the put is there, so it is not registered. No errors are showing though.
This is a react-native application and I am currently writing some end-to-end testing.
A token is stored in the redux store shown below and I am testing the login functionality using detox/jest. I need to detect if the token exists in the store in my login.spec.js . If the token exists I want to wipe it from the store so the user is not logged in automatically when i reload the app to take the user back to another scene. The main function in question is the refreshUserToken() and line:-
const { refresh_token } = yield select(token);
Here is the redux saga file User.js located at:-MyApp/App/Sagas/User.js
import { call, put, takeEvery, select } from "redux-saga/effects";
import Config from "MyApp/App/Config";
import API from "MyApp/App/Services/API";
import { when } from "MyApp/App/Helpers/Predicate";
import Credentials from "MyApp/App/Helpers/Credentials";
import ActionCreator from "MyApp/App/Actions";
const appendPayload = payload => {
return {
...payload,
// Removed because no longer needed unless for testing purposes.
// username: Config.TEST_USERNAME,
// password: Config.TEST_PASSWORD,
client_id: Config.CLIENT_ID,
client_secret: Config.CLIENT_SECRET,
};
};
const token = state => state.token;
const user = state => state.user;
const attemptUserLogin = function*(action) {
const { payload } = action;
const login = "/oauth/token";
const grant_type = "password";
const loginPayload = appendPayload(payload);
action.payload = {
...loginPayload,
grant_type,
};
yield attemptUserAuthorisation(login, action);
};
const attemptUserRegister = function*(action) {
const register = "/api/signup";
const { payload } = action;
yield Credentials.save(payload);
yield put(ActionCreator.saveUserCredentials(payload));
yield attemptUserAuthorisation(register, action);
};
const refreshUserToken = function*(action) {
const login = "/oauth/token";
const grant_type = "refresh_token";
const { refresh_token } = yield select(token);
action.payload = {
...action.payload,
grant_type,
refresh_token,
};
yield attemptUserAuthorisation(login, action);
};
const watchExampleSaga = function*() {
yield takeEvery(ActionCreator.AUTO_USER_LOGIN, autoUserLogin);
yield takeEvery(ActionCreator.USER_LOGIN, attemptUserLogin);
yield takeEvery(ActionCreator.USER_REGISTER, attemptUserRegister);
yield takeEvery(ActionCreator.USER_REFRESH_TOKEN, refreshUserToken);
};
export default watchExampleSaga;
Here is my detox/jest spec file located at:-MyApp/App/e2e/login.spec.js
describe('Login Actions', () => {
it('Should be able to enter an email address', async () => {
await element(by.id('landing-login-btn')).tap()
const email = 'banker#dovu.io'
await element(by.id('login-email')).tap()
await element(by.id('login-email')).replaceText(email)
});
it('Should be able to enter a password', async () => {
const password = 'secret'
await element(by.id('login-password')).tap()
await element(by.id('login-password')).replaceText(password)
});
it('Should be able to click the continue button and login', async () => {
await element(by.id('login-continue-btn')).tap()
await waitFor(element(by.id('dashboard-logo'))).toBeVisible().withTimeout(500)
// If token exists destroy it and relaunch app. This is where I need to grab the token from the redux saga!
await device.launchApp({newInstance: true});
});
})
This is how I handled a similar scenario:
in package.json scripts:
"start:detox": "RN_SRC_EXT=e2e.tsx,e2e.ts node node_modules/react-native/local-cli/cli.js start",
In my detox config:
"build": "ENVFILE=.env.dev;RN_SRC_EXT=e2e.tsx,e2e,ts npx react-native run-ios --simulator='iPhone 7'",
That lets me write MyFile.e2e.tsx which replaces MyFile.tsx whilst detox is running
In the test version of that component I have buttons which are tapped in the tests and the buttons dispatch redux actions
Looks like this actually cant be done unless someone can give me a solution other than mocking the state which still wouldn't work in this case my app checks for real states to auto login.
I did get to the stage of creating a new action getUserToken and exporting that into my jest file. However the action returns undefined because the jest file requires a dispatch method like in containers.js. If anyone could provide me with a method of this using jest I would be very happy.
I’m trying to test an axios call in a debounced method, but moxios.wait() always times out if I add fake timers.
The test works without the clock, if the debounce time is set small enough (e.g. 10ms) but that doesn’t help testing proper debouncing.
I’ve tried experimenting with Vue.nextTick as well as making the callback to it() async, but I seem to only go further into the weeds. What’s the right approach here?
Here’s a component and test in one, that shows the problem:
import Vue from 'vue'
import { mount } from 'vue-test-utils'
import axios from 'axios'
import moxios from 'moxios'
import _ from 'lodash'
import expect from 'expect'
import sinon from 'sinon'
let Debounced = Vue.component('Debounced',
{
template: `<div><button #click.prevent="fetch"></button></div>`,
data() {
return {
data: {}
}
},
methods: {
fetch: _.debounce(async () => {
let data = await axios.post('/test', {param: 'example'})
this.data = data
}, 100)
}
}
)
describe.only ('Test axios in debounce()', () => {
let wrapper, clock
beforeEach(() => {
clock = sinon.useFakeTimers()
moxios.install()
wrapper = mount(Debounced)
})
afterEach(() => {
moxios.uninstall()
clock.restore()
})
it ('should send off a request when clicked', (done) => {
// Given we set up axios to return something
moxios.stubRequest('/test', {
status: 200,
response: []
})
// When the button is clicked
wrapper.find('button').trigger('click')
clock.tick(100)
moxios.wait(() => {
// It should have called axios with the right params
let request = moxios.requests.mostRecent()
expect(JSON.parse(request.config.data)).toEqual({param: 'example'})
done()
})
})
})
About test timeout exception: moxios.wait relies on the setTimeout but we replaced our setTimeout with custom js implementation, and to make moxios.waitwork we should invoke clock.tick(1) after wait call.
moxios.wait(() => {
// It should have called axios with the right params
let request = moxios.requests.mostRecent()
expect(JSON.parse(request.config.data)).toEqual({param: 'example'})
done()
});
clock.tick(1);
This will allow test to enter the callback body...But it will fail again with exception that request is undefined.
Main problem: The problem is that fake timers are calling all callbacks synchronously(without using macrotask event loop) but Promises still uses event loop.
So your code just ends before any Promise "then" will be executed. Of course you can push your test code as microtasks to access request and response data, for example (using await or wrap it as then" callback):
wrapper.find('button').trigger('click');
// debounced function called
clock.tick(100);
// next lines will be executed after an axios "then" callback where a request will be created.
await Promise.resolve();
// axios promise created, a request is added to moxios requests
// next "then" is added to microtask event loop
console.log("First assert");
let request = moxios.requests.mostRecent()
expect(JSON.parse(request.config.data)).toEqual({ param: 'example' });
// next lines will be executed after promise "then" from fetch method
await Promise.resolve();
console.log("Second assert");
expect(wrapper.vm.data.data).toEqual([]);
done();
I added another assertion for data to show that your code should "wait" for 2 "then" callbacks: axios internal "then" with your request creation and your "then" from fetch method.
As you see I removed wait call because it actually do nothing if you want to wait for Promises.
Yes, this is a dirty hack and closely related to axios implementation itself, but I don't have better advice and firstly I just tried to explain the issue.