Merge branch 'master' into fix-scroll-login-page

This commit is contained in:
Uzair Rasheed
2021-02-05 15:08:15 +05:00
committed by GitHub
13 changed files with 30 additions and 278 deletions

View File

@@ -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?"`;

View File

@@ -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;
};
}

View File

@@ -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);
});
});

View File

@@ -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';

View File

@@ -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);
}

View File

@@ -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');
});
});

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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));