Tests for the saga, AsyncActionType, and profile actions.
Also changing the way the saga gets ahold of the UserAccountApiService to make it more testable.
This commit is contained in:
@@ -10,6 +10,10 @@ export default class AsyncActionType {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
get BASE() {
|
||||
return `${this.topic}__${this.name}`;
|
||||
}
|
||||
|
||||
get BEGIN() {
|
||||
return `${this.topic}__${this.name}__BEGIN`;
|
||||
}
|
||||
@@ -25,10 +29,4 @@ export default class AsyncActionType {
|
||||
get RESET() {
|
||||
return `${this.topic}__${this.name}__RESET`;
|
||||
}
|
||||
|
||||
get BASE() {
|
||||
return `${this.topic}__${this.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
AsyncActionType.prototype.toString = () => `${this.topic}__${this.name}`;
|
||||
|
||||
13
src/actions/AsyncActionType.test.js
Normal file
13
src/actions/AsyncActionType.test.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import AsyncActionType from './AsyncActionType';
|
||||
|
||||
describe('AsyncActionType', () => {
|
||||
it('should return well formatted action strings', () => {
|
||||
const actionType = new AsyncActionType('HOUSE_CATS', 'START_THE_RACE');
|
||||
|
||||
expect(actionType.BASE).toBe('HOUSE_CATS__START_THE_RACE');
|
||||
expect(actionType.BEGIN).toBe('HOUSE_CATS__START_THE_RACE__BEGIN');
|
||||
expect(actionType.SUCCESS).toBe('HOUSE_CATS__START_THE_RACE__SUCCESS');
|
||||
expect(actionType.FAILURE).toBe('HOUSE_CATS__START_THE_RACE__FAILURE');
|
||||
expect(actionType.RESET).toBe('HOUSE_CATS__START_THE_RACE__RESET');
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,8 @@
|
||||
import {
|
||||
openEditableField,
|
||||
closeEditableField,
|
||||
EDITABLE_FIELD_OPEN,
|
||||
EDITABLE_FIELD_CLOSE,
|
||||
SAVE_USER_PROFILE,
|
||||
saveUserProfileBegin,
|
||||
saveUserProfileSuccess,
|
||||
@@ -7,6 +11,24 @@ import {
|
||||
saveUserProfile,
|
||||
} from './profile';
|
||||
|
||||
describe('editable field actions', () => {
|
||||
it('should create an open action', () => {
|
||||
const expectedAction = {
|
||||
type: EDITABLE_FIELD_OPEN,
|
||||
fieldName: 'name',
|
||||
};
|
||||
expect(openEditableField('name')).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create a closed action', () => {
|
||||
const expectedAction = {
|
||||
type: EDITABLE_FIELD_CLOSE,
|
||||
fieldName: 'name',
|
||||
};
|
||||
expect(closeEditableField('name')).toEqual(expectedAction);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SAVE profile actions', () => {
|
||||
const userAccountState = {
|
||||
username: 'verified',
|
||||
|
||||
@@ -3,6 +3,7 @@ import createSagaMiddleware from 'redux-saga';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import { UserAccountApiService } from '@edx/frontend-auth';
|
||||
|
||||
import apiClient from './apiClient';
|
||||
import reducers from './reducers/RootReducer';
|
||||
@@ -17,6 +18,9 @@ const store = createStore(
|
||||
composeWithDevTools(applyMiddleware(thunkMiddleware, sagaMiddleware, loggerMiddleware)),
|
||||
);
|
||||
|
||||
sagaMiddleware.run(rootSaga);
|
||||
const apiService = new UserAccountApiService(apiClient, process.env.LMS_BASE_URL);
|
||||
apiService.saveUserAccount = apiService.saveUserAccount.bind(apiService);
|
||||
|
||||
sagaMiddleware.run(rootSaga, apiService);
|
||||
|
||||
export default store;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { call, put, takeEvery, delay } from 'redux-saga/effects';
|
||||
import { UserAccountApiService } from '@edx/frontend-auth';
|
||||
|
||||
import {
|
||||
SAVE_USER_PROFILE,
|
||||
@@ -9,9 +8,8 @@ import {
|
||||
saveUserProfileReset,
|
||||
closeEditableField,
|
||||
} from '../actions/profile';
|
||||
import apiClient from '../data/apiClient';
|
||||
|
||||
const apiService = new UserAccountApiService(apiClient, process.env.LMS_BASE_URL);
|
||||
let userAccountApiService = null;
|
||||
|
||||
const PROP_TO_STATE_MAP = {
|
||||
fullName: 'name',
|
||||
@@ -20,7 +18,7 @@ const PROP_TO_STATE_MAP = {
|
||||
socialLinks: socialLinks => socialLinks.filter(({ socialLink }) => socialLink !== null),
|
||||
};
|
||||
|
||||
const mapDataForRequest = (props) => {
|
||||
export const mapDataForRequest = (props) => {
|
||||
const state = {};
|
||||
Object.keys(props).forEach((prop) => {
|
||||
const propModifier = PROP_TO_STATE_MAP[prop] || prop;
|
||||
@@ -33,12 +31,12 @@ const mapDataForRequest = (props) => {
|
||||
return state;
|
||||
};
|
||||
|
||||
function* saveUserProfile(action) {
|
||||
export function* saveUserProfile(action) {
|
||||
try {
|
||||
yield put(saveUserProfileBegin());
|
||||
const userAccount = yield call(
|
||||
// Because apiService is a class, 'this' needs to be bound.
|
||||
apiService.saveUserAccount.bind(apiService),
|
||||
userAccountApiService.saveUserAccount,
|
||||
action.payload.username,
|
||||
mapDataForRequest(action.payload.userAccountState),
|
||||
);
|
||||
@@ -60,6 +58,7 @@ function* saveUserProfile(action) {
|
||||
}
|
||||
}
|
||||
|
||||
export default function* rootSaga() {
|
||||
export default function* rootSaga(apiService) {
|
||||
userAccountApiService = apiService;
|
||||
yield takeEvery(SAVE_USER_PROFILE.BASE, saveUserProfile);
|
||||
}
|
||||
|
||||
121
src/sagas/RootSaga.test.js
Normal file
121
src/sagas/RootSaga.test.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import { takeEvery, put, call, delay } from 'redux-saga/effects';
|
||||
|
||||
import rootSaga, { saveUserProfile, mapDataForRequest } from './RootSaga';
|
||||
import * as profileActions from '../actions/profile';
|
||||
|
||||
class MockUserAccountApiService {
|
||||
constructor() {
|
||||
this.saveUserAccount = jest.fn();
|
||||
}
|
||||
}
|
||||
|
||||
describe('RootSaga', () => {
|
||||
describe('rootSaga', () => {
|
||||
it('should pass actions to the correct sagas', () => {
|
||||
const gen = rootSaga();
|
||||
// There is only one.
|
||||
expect(gen.next().value)
|
||||
.toEqual(takeEvery(profileActions.SAVE_USER_PROFILE.BASE, saveUserProfile));
|
||||
// ... and done.
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveUserProfile', () => {
|
||||
it('should successfully process a saveUserProfile request if there are no exceptions', () => {
|
||||
const service = new MockUserAccountApiService();
|
||||
const rootGen = rootSaga(service);
|
||||
rootGen.next(); // Causes the service to be set.
|
||||
|
||||
const action = profileActions.saveUserProfile(
|
||||
'my username',
|
||||
{
|
||||
fullName: 'Full Name',
|
||||
education: 'b',
|
||||
},
|
||||
'ze field',
|
||||
);
|
||||
const gen = saveUserProfile(action);
|
||||
const userAccount = {
|
||||
name: 'Full Name',
|
||||
levelOfEducation: 'b',
|
||||
};
|
||||
expect(gen.next().value).toEqual(put(profileActions.saveUserProfileBegin()));
|
||||
expect(gen.next().value).toEqual(call(service.saveUserAccount, 'my username', userAccount));
|
||||
// The library would supply the result of the above call
|
||||
// as the parameter to the NEXT yield. Here:
|
||||
expect(gen.next(userAccount).value).toEqual(put(profileActions.saveUserProfileSuccess()));
|
||||
expect(gen.next().value).toEqual(put({
|
||||
type: 'FETCH_USER_ACCOUNT_SUCCESS',
|
||||
payload: { userAccount },
|
||||
}));
|
||||
expect(gen.next().value).toEqual(delay(300));
|
||||
expect(gen.next().value).toEqual(put(profileActions.closeEditableField('ze field')));
|
||||
expect(gen.next().value).toEqual(delay(300));
|
||||
expect(gen.next().value).toEqual(put(profileActions.saveUserProfileReset()));
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should successfully publish a failure action on exception', () => {
|
||||
const service = new MockUserAccountApiService();
|
||||
const error = new Error('uhoh');
|
||||
const rootGen = rootSaga(service);
|
||||
rootGen.next(); // Causes the service to be set.
|
||||
|
||||
const action = profileActions.saveUserProfile(
|
||||
'my username',
|
||||
{
|
||||
fullName: 'Full Name',
|
||||
education: 'b',
|
||||
},
|
||||
'ze field',
|
||||
);
|
||||
const gen = saveUserProfile(action);
|
||||
|
||||
expect(gen.next().value).toEqual(put(profileActions.saveUserProfileBegin()));
|
||||
const result = gen.throw(error);
|
||||
expect(result.value).toEqual(put(profileActions.saveUserProfileFailure(error)));
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapDataForRequest', () => {
|
||||
it('should modify props according to prop modifier strings and functions', () => {
|
||||
const props = {
|
||||
favoriteColor: 'red',
|
||||
age: 30,
|
||||
petName: 'Donkey',
|
||||
fullName: 'Donkey McWafflebatter',
|
||||
userLocation: 'US',
|
||||
education: 'BS',
|
||||
socialLinks: {
|
||||
twitter: {
|
||||
url: 'https://www.twitter.com',
|
||||
},
|
||||
facebook: {
|
||||
url: 'https://www.facebook.com',
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = mapDataForRequest(props);
|
||||
expect(result).toEqual({
|
||||
favoriteColor: 'red',
|
||||
age: 30,
|
||||
petName: 'Donkey',
|
||||
name: 'Donkey McWafflebatter',
|
||||
country: 'US',
|
||||
levelOfEducation: 'BS',
|
||||
socialLinks: [
|
||||
{
|
||||
platform: 'twitter',
|
||||
socialLink: 'https://www.twitter.com',
|
||||
},
|
||||
{
|
||||
platform: 'facebook',
|
||||
socialLink: 'https://www.facebook.com',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user