Merge branch 'master' into fix-scroll-login-page
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getModuleState should throw an exception on a bad path 1`] = `"Unexpected state key uhoh given to getModuleState. Is your state path set up correctly?"`;
|
||||
@@ -1,46 +1,9 @@
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import snakeCase from 'lodash.snakecase';
|
||||
// Utility functions
|
||||
|
||||
export function modifyObjectKeys(object, modify) {
|
||||
// If the passed in object is not an object, return it.
|
||||
if (
|
||||
object === undefined
|
||||
|| object === null
|
||||
|| (typeof object !== 'object' && !Array.isArray(object))
|
||||
) {
|
||||
return object;
|
||||
}
|
||||
|
||||
if (Array.isArray(object)) {
|
||||
return object.map(value => modifyObjectKeys(value, modify));
|
||||
}
|
||||
|
||||
// Otherwise, process all its keys.
|
||||
const result = {};
|
||||
Object.entries(object).forEach(([key, value]) => {
|
||||
result[modify(key)] = modifyObjectKeys(value, modify);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function camelCaseObject(object) {
|
||||
return modifyObjectKeys(object, camelCase);
|
||||
}
|
||||
|
||||
export function snakeCaseObject(object) {
|
||||
return modifyObjectKeys(object, snakeCase);
|
||||
}
|
||||
|
||||
export function convertKeyNames(object, nameMap) {
|
||||
const transformer = key => (nameMap[key] === undefined ? key : nameMap[key]);
|
||||
|
||||
return modifyObjectKeys(object, transformer);
|
||||
}
|
||||
|
||||
export const processLink = (link) => {
|
||||
export default function processLink(link) {
|
||||
let matches;
|
||||
link.replace(/(.*?)<a href=["']([^"']*).*?>([^<]+)<\/a>(.*)/g, function () { // eslint-disable-line func-names
|
||||
matches = Array.prototype.slice.call(arguments, 1, 5); // eslint-disable-line prefer-rest-params
|
||||
});
|
||||
return matches;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,90 +1,14 @@
|
||||
import {
|
||||
modifyObjectKeys,
|
||||
camelCaseObject,
|
||||
snakeCaseObject,
|
||||
convertKeyNames,
|
||||
} from './dataUtils';
|
||||
import processLink from './dataUtils';
|
||||
|
||||
describe('modifyObjectKeys', () => {
|
||||
it('should use the provided modify function to change all keys in and object and its children', () => {
|
||||
function meowKeys(key) {
|
||||
return `${key}Meow`;
|
||||
}
|
||||
describe('processLink', () => {
|
||||
it('should use the provided processLink function to', () => {
|
||||
const expectedHref = 'http://test.server.com/';
|
||||
const expectedText = 'test link';
|
||||
const link = `<a href="${expectedHref}">${expectedText}</a>`;
|
||||
|
||||
const result = modifyObjectKeys(
|
||||
{
|
||||
one: undefined,
|
||||
two: null,
|
||||
three: '',
|
||||
four: 0,
|
||||
five: NaN,
|
||||
six: [1, 2, { seven: 'woof' }],
|
||||
eight: { nine: { ten: 'bark' }, eleven: true },
|
||||
},
|
||||
meowKeys,
|
||||
);
|
||||
const matches = processLink(link);
|
||||
|
||||
expect(result).toEqual({
|
||||
oneMeow: undefined,
|
||||
twoMeow: null,
|
||||
threeMeow: '',
|
||||
fourMeow: 0,
|
||||
fiveMeow: NaN,
|
||||
sixMeow: [1, 2, { sevenMeow: 'woof' }],
|
||||
eightMeow: { nineMeow: { tenMeow: 'bark' }, elevenMeow: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('camelCaseObject', () => {
|
||||
it('should make everything camelCase', () => {
|
||||
const result = camelCaseObject({
|
||||
what_now: 'brown cow',
|
||||
but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
|
||||
'dot.dot.dot': 123,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
whatNow: 'brown cow',
|
||||
butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
|
||||
dotDotDot: 123,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('snakeCaseObject', () => {
|
||||
it('should make everything snake_case', () => {
|
||||
const result = snakeCaseObject({
|
||||
whatNow: 'brown cow',
|
||||
butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
|
||||
'dot.dot.dot': 123,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
what_now: 'brown cow',
|
||||
but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
|
||||
dot_dot_dot: 123,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertKeyNames', () => {
|
||||
it('should replace the specified keynames', () => {
|
||||
const result = convertKeyNames(
|
||||
{
|
||||
one: { two: { three: 'four' } },
|
||||
five: 'six',
|
||||
},
|
||||
{
|
||||
two: 'blue',
|
||||
five: 'alive',
|
||||
seven: 'heaven',
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
one: { blue: { three: 'four' } },
|
||||
alive: 'six',
|
||||
});
|
||||
expect(matches[1]).toEqual(expectedHref);
|
||||
expect(matches[2]).toEqual(expectedText);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,2 @@
|
||||
export {
|
||||
camelCaseObject,
|
||||
convertKeyNames,
|
||||
modifyObjectKeys,
|
||||
snakeCaseObject,
|
||||
} from './dataUtils';
|
||||
export {
|
||||
AsyncActionType,
|
||||
getModuleState,
|
||||
} from './reduxUtils';
|
||||
export { default as handleFailure } from './sagaUtils';
|
||||
export { unpackFieldErrors, handleRequestError } from './serviceUtils';
|
||||
export { default } from './dataUtils';
|
||||
export { default as AsyncActionType } from './reduxUtils';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Helper class to save time when writing out action types for asynchronous methods. Also helps
|
||||
* ensure that actions are namespaced.
|
||||
*/
|
||||
export class AsyncActionType {
|
||||
export default class AsyncActionType {
|
||||
constructor(topic, name) {
|
||||
this.topic = topic;
|
||||
this.name = name;
|
||||
@@ -32,35 +32,3 @@ export class AsyncActionType {
|
||||
return `${this.topic}__${this.name}__FORBIDDEN`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a state tree and an array representing a set of keys to traverse in that tree, returns
|
||||
* the portion of the tree at that key path.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* const result = getModuleState(
|
||||
* {
|
||||
* first: { red: { awesome: 'sauce' }, blue: { weak: 'sauce' } },
|
||||
* second: { other: 'data', }
|
||||
* },
|
||||
* ['first', 'red']
|
||||
* );
|
||||
*
|
||||
* result will be:
|
||||
*
|
||||
* {
|
||||
* awesome: 'sauce'
|
||||
* }
|
||||
*/
|
||||
export function getModuleState(state, originalPath) {
|
||||
const path = [...originalPath]; // don't modify your argument
|
||||
if (path.length < 1) {
|
||||
return state;
|
||||
}
|
||||
const key = path.shift();
|
||||
if (state[key] === undefined) {
|
||||
throw new Error(`Unexpected state key ${key} given to getModuleState. Is your state path set up correctly?`);
|
||||
}
|
||||
return getModuleState(state[key], path);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
AsyncActionType,
|
||||
getModuleState,
|
||||
} from './reduxUtils';
|
||||
import AsyncActionType from './reduxUtils';
|
||||
|
||||
describe('AsyncActionType', () => {
|
||||
it('should return well formatted action strings', () => {
|
||||
@@ -15,38 +12,3 @@ describe('AsyncActionType', () => {
|
||||
expect(actionType.FORBIDDEN).toBe('HOUSE_CATS__START_THE_RACE__FORBIDDEN');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getModuleState', () => {
|
||||
const state = {
|
||||
first: { red: { awesome: 'sauce' }, blue: { weak: 'sauce' } },
|
||||
second: { other: 'data' },
|
||||
};
|
||||
|
||||
it('should return everything if given an empty path', () => {
|
||||
expect(getModuleState(state, [])).toEqual(state);
|
||||
});
|
||||
|
||||
it('should resolve paths correctly', () => {
|
||||
expect(getModuleState(
|
||||
state,
|
||||
['first'],
|
||||
)).toEqual({ red: { awesome: 'sauce' }, blue: { weak: 'sauce' } });
|
||||
|
||||
expect(getModuleState(
|
||||
state,
|
||||
['first', 'red'],
|
||||
)).toEqual({ awesome: 'sauce' });
|
||||
|
||||
expect(getModuleState(state, ['second'])).toEqual({ other: 'data' });
|
||||
});
|
||||
|
||||
it('should throw an exception on a bad path', () => {
|
||||
expect(() => {
|
||||
getModuleState(state, ['uhoh']);
|
||||
}).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
it('should return non-objects correctly', () => {
|
||||
expect(getModuleState(state, ['first', 'red', 'awesome'])).toEqual('sauce');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { put } from 'redux-saga/effects';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
|
||||
export default function* handleFailure(error, failureAction = null, failureRedirectPath = null) {
|
||||
if (error.fieldErrors && failureAction !== null) {
|
||||
yield put(failureAction({ fieldErrors: error.fieldErrors }));
|
||||
}
|
||||
logError(error);
|
||||
if (failureAction !== null) {
|
||||
yield put(failureAction(error.message));
|
||||
}
|
||||
if (failureRedirectPath !== null) {
|
||||
history.push(failureRedirectPath);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* Turns field errors of the form:
|
||||
*
|
||||
* {
|
||||
* "name":{
|
||||
* "developer_message": "Nerdy message here",
|
||||
* "user_message": "This value is invalid."
|
||||
* },
|
||||
* "other_field": {
|
||||
* "developer_message": "Other Nerdy message here",
|
||||
* "user_message": "This other value is invalid."
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Into:
|
||||
*
|
||||
* {
|
||||
* "name": "This value is invalid.",
|
||||
* "other_field": "This other value is invalid"
|
||||
* }
|
||||
*/
|
||||
export function unpackFieldErrors(fieldErrors) {
|
||||
return Object.entries(fieldErrors).reduce((acc, [k, v]) => {
|
||||
acc[k] = v.user_message;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes and re-throws request errors. If the response contains a field_errors field, will
|
||||
* massage the data into a form expected by the client.
|
||||
*
|
||||
* Field errors will be packaged as an api error with a fieldErrors field usable by the client.
|
||||
* Takes an optional unpack function which is used to process the field errors,
|
||||
* otherwise uses the default unpackFieldErrors function.
|
||||
*
|
||||
* @param error The original error object.
|
||||
* @param unpackFunction (Optional) A function to use to unpack the field errors as a replacement
|
||||
* for the default.
|
||||
*/
|
||||
export function handleRequestError(error, unpackFunction = unpackFieldErrors) {
|
||||
if (error.response && error.response.data.field_errors) {
|
||||
const apiError = Object.create(error);
|
||||
apiError.fieldErrors = unpackFunction(error.response.data.field_errors);
|
||||
throw apiError;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
// Actions
|
||||
@@ -25,6 +26,7 @@ export function* handleForgotPassword(action) {
|
||||
} else {
|
||||
yield put(forgotPasswordServerError());
|
||||
}
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/
|
||||
import { Alert } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { processLink } from '../data/utils/dataUtils';
|
||||
import processLink from '../data/utils';
|
||||
import {
|
||||
FORBIDDEN_REQUEST,
|
||||
INACTIVE_USER,
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
REGISTRATION_EXTRA_FIELDS,
|
||||
} from '../data/constants';
|
||||
import messages from './messages';
|
||||
import { processLink } from '../data/utils/dataUtils';
|
||||
import processLink from '../data/utils';
|
||||
|
||||
class RegistrationPage extends React.Component {
|
||||
constructor(props, context) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
// Actions
|
||||
import {
|
||||
@@ -27,6 +28,7 @@ export function* handleValidateToken(action) {
|
||||
}
|
||||
} catch (err) {
|
||||
yield put(validateTokenFailure(err));
|
||||
logError(err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +46,7 @@ export function* handleResetPassword(action) {
|
||||
}
|
||||
} catch (err) {
|
||||
yield put(resetPasswordFailure(err));
|
||||
logError(err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ import {
|
||||
} from '../actions';
|
||||
import { handleResetPassword } from '../sagas';
|
||||
import * as api from '../service';
|
||||
import initializeMockLogging from '../../../setupTest';
|
||||
|
||||
const { loggingService } = initializeMockLogging();
|
||||
|
||||
describe('handleResetPassword', () => {
|
||||
const params = {
|
||||
@@ -25,6 +28,10 @@ describe('handleResetPassword', () => {
|
||||
err_msg: '',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
loggingService.logError.mockReset();
|
||||
});
|
||||
|
||||
it('should call service and dispatch success action', async () => {
|
||||
const resetPassword = jest.spyOn(api, 'resetPassword')
|
||||
.mockImplementation(() => Promise.resolve(responseData));
|
||||
|
||||
Reference in New Issue
Block a user