refactor: update test coverage (#202)
* clean up and test segment integration * add tests for action utils * add tests for store aggregator and utils * clean up un-used paths in thunkAction testUtils * clean up filter reducer coverage * add filter reducer tests for filterMenu actions * clean up grades selectors coverage * separate App and add unit tests * ignore external files in coverage analysis * remove old/unused code from StrictDict and clean up tests * clean up FileUploadForm coverage * more cleanup for StrictDict tests * clean up GradesTab test coverage * clean up GradesTab coverage * ignore reducer-mapping for unit-test coverage * clean up AssignmentFilter test coverage * add index-level test coverage * temp remove snapshots * re-add App snapshot * v1.4.41
This commit is contained in:
36
src/App.jsx
Executable file
36
src/App.jsx
Executable file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
|
||||
import { routePath } from 'data/constants/app';
|
||||
import store from 'data/store';
|
||||
import GradebookPage from 'containers/GradebookPage';
|
||||
import EdxHeader from 'components/EdxHeader';
|
||||
import './App.scss';
|
||||
|
||||
const App = () => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<div>
|
||||
<EdxHeader />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={routePath}
|
||||
component={GradebookPage}
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
</div>
|
||||
</Router>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
export default App;
|
||||
86
src/App.test.jsx
Normal file
86
src/App.test.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
|
||||
import { routePath } from 'data/constants/app';
|
||||
import store from 'data/store';
|
||||
import GradebookPage from 'containers/GradebookPage';
|
||||
import EdxHeader from 'components/EdxHeader';
|
||||
|
||||
import App from './App';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
BrowserRouter: () => 'BrowserRouter',
|
||||
Route: () => 'Route',
|
||||
Switch: () => 'Switch',
|
||||
}));
|
||||
jest.mock('react-redux', () => ({
|
||||
Provider: () => 'Provider',
|
||||
}));
|
||||
jest.mock('react-intl', () => ({
|
||||
IntlProvider: () => 'IntlProvider',
|
||||
}));
|
||||
jest.mock('data/constants/app', () => ({
|
||||
routePath: '/:courseId',
|
||||
}));
|
||||
jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
jest.mock('data/store', () => 'testStore');
|
||||
jest.mock('containers/GradebookPage', () => 'GradebookPage');
|
||||
jest.mock('components/EdxHeader', () => 'EdxHeader');
|
||||
|
||||
const logo = 'fakeLogo.png';
|
||||
let el;
|
||||
let router;
|
||||
|
||||
describe('App router component', () => {
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<App />)).toMatchSnapshot();
|
||||
});
|
||||
describe('component', () => {
|
||||
beforeEach(() => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
el = shallow(<App />);
|
||||
router = el.childAt(0).childAt(0);
|
||||
});
|
||||
describe('IntlProvider', () => {
|
||||
test('outer-wrapper component', () => {
|
||||
expect(el.type()).toBe(IntlProvider);
|
||||
});
|
||||
test('"en" locale', () => {
|
||||
expect(el.props().locale).toEqual('en');
|
||||
});
|
||||
});
|
||||
describe('Provider, inside IntlProvider', () => {
|
||||
test('first child, passed the redux store props', () => {
|
||||
expect(el.childAt(0).type()).toBe(Provider);
|
||||
expect(el.childAt(0).props().store).toEqual(store);
|
||||
});
|
||||
});
|
||||
describe('Router', () => {
|
||||
test('first child of Provider', () => {
|
||||
expect(router.type()).toBe(Router);
|
||||
});
|
||||
test('EdxHeader is above/outside-of the routing', () => {
|
||||
expect(router.childAt(0).childAt(0).type()).toBe(EdxHeader);
|
||||
expect(router.childAt(0).childAt(1).type()).toBe('main');
|
||||
});
|
||||
test('Routing - GradebookPage is only route', () => {
|
||||
expect(router.find('main')).toEqual(shallow(
|
||||
<main>
|
||||
<Switch>
|
||||
<Route exact path={routePath} component={GradebookPage} />
|
||||
</Switch>
|
||||
</main>,
|
||||
));
|
||||
});
|
||||
});
|
||||
test('Footer logo drawn from env variable', () => {
|
||||
expect(router.find(Footer).props().logo).toEqual(logo);
|
||||
});
|
||||
});
|
||||
});
|
||||
27
src/__snapshots__/App.test.jsx.snap
Normal file
27
src/__snapshots__/App.test.jsx.snap
Normal file
@@ -0,0 +1,27 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App router component snapshot 1`] = `
|
||||
<IntlProvider
|
||||
locale="en"
|
||||
>
|
||||
<Provider
|
||||
store="testStore"
|
||||
>
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<EdxHeader />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
component="GradebookPage"
|
||||
exact={true}
|
||||
path="/:courseId"
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
`;
|
||||
@@ -114,6 +114,14 @@ describe('FileUploadForm', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('fileInput helper', () => {
|
||||
test('links to fileInputRef.current', () => {
|
||||
el = mount(<FileUploadForm {...props} />);
|
||||
const ref = 'a-fake-ref';
|
||||
el.instance().fileInputRef = { current: ref };
|
||||
expect(el.instance().fileInput).toEqual(ref);
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
let fileInput;
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -69,7 +69,6 @@ describe('AssignmentFilter', () => {
|
||||
el = mount(<AssignmentFilter {...props} />);
|
||||
el.instance().handleChange(event);
|
||||
});
|
||||
|
||||
it('calls props.updateAssignmentFilter with selection', () => {
|
||||
expect(props.updateAssignmentFilter).toHaveBeenCalledWith({
|
||||
label: newAssgn,
|
||||
@@ -87,6 +86,30 @@ describe('AssignmentFilter', () => {
|
||||
const method = props.fetchGradesIfAssignmentGradeFiltersSet;
|
||||
expect(method).toHaveBeenCalledWith();
|
||||
});
|
||||
describe('no selected option', () => {
|
||||
const value = 'fake';
|
||||
beforeEach(() => {
|
||||
el = mount(<AssignmentFilter {...props} />);
|
||||
el.instance().handleChange({ target: { value } });
|
||||
});
|
||||
it('calls props.updateAssignmentFilter with selection', () => {
|
||||
expect(props.updateAssignmentFilter).toHaveBeenCalledWith({
|
||||
label: value,
|
||||
type: undefined,
|
||||
id: undefined,
|
||||
});
|
||||
});
|
||||
it('calls props.updateQueryParams with selected assignment id',
|
||||
() => {
|
||||
expect(props.updateQueryParams).toHaveBeenCalledWith({
|
||||
assignment: undefined,
|
||||
});
|
||||
});
|
||||
it('calls props.fetchGradesIfAssignmentGradeFiltersSet', () => {
|
||||
const method = props.fetchGradesIfAssignmentGradeFiltersSet;
|
||||
expect(method).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import thunkActions from 'data/thunkActions';
|
||||
|
||||
import {
|
||||
GradesTab,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from '.';
|
||||
|
||||
@@ -78,6 +79,9 @@ describe('GradesTab', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
test('mapStateToProps is empty', () => {
|
||||
expect(mapStateToProps({ some: 'state' })).toEqual({});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
describe('fetchGrades', () => {
|
||||
test('from thunkActions.grades.fetchGrades', () => {
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
export const options = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
timeZone: 'UTC',
|
||||
};
|
||||
export const timeOptions = {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZone: 'UTC',
|
||||
timeZoneName: 'short',
|
||||
};
|
||||
|
||||
const formatDateForDisplay = (inputDate) => {
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
timeZone: 'UTC',
|
||||
};
|
||||
const timeOptions = {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZone: 'UTC',
|
||||
timeZoneName: 'short',
|
||||
};
|
||||
return `${inputDate.toLocaleDateString('en-US', options)} at ${inputDate.toLocaleTimeString('en-US', timeOptions)}`;
|
||||
const date = inputDate.toLocaleDateString('en-US', options);
|
||||
const time = inputDate.toLocaleTimeString('en-US', timeOptions);
|
||||
return `${date} at ${time}`;
|
||||
};
|
||||
|
||||
const sortAlphaAsc = (gradeRowA, gradeRowB) => {
|
||||
|
||||
34
src/data/actions/utils.test.js
Normal file
34
src/data/actions/utils.test.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import * as utils from './utils';
|
||||
|
||||
jest.mock('@reduxjs/toolkit', () => ({
|
||||
createAction: (key, ...args) => ({ action: key, args }),
|
||||
}));
|
||||
|
||||
describe('redux action utils', () => {
|
||||
describe('formatDateForDisplay', () => {
|
||||
it('returns the datetime as a formatted string', () => {
|
||||
expect(utils.formatDateForDisplay(new Date('Jun 3 2021 11:59 AM EDT'))).toEqual(
|
||||
'June 3, 2021 at 03:59 PM UTC',
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('sortAlphaAsc', () => {
|
||||
it('returns sorting value (-1, 0, 1) by uppercase username', () => {
|
||||
const sort = (v1, v2) => utils.sortAlphaAsc({ username: v1 }, { username: v2 });
|
||||
expect(sort('aName', 'ANAme')).toEqual(0);
|
||||
expect(sort('aName', 'laterName')).toEqual(-1);
|
||||
expect(sort('laterName', 'aName')).toEqual(1);
|
||||
});
|
||||
});
|
||||
describe('createActionFactory', () => {
|
||||
it('returns an action creator with the data key', () => {
|
||||
const dataKey = 'part-of-the-model';
|
||||
const actionKey = 'an-action';
|
||||
const args = ['some', 'args'];
|
||||
expect(utils.createActionFactory(dataKey)(actionKey, ...args)).toEqual(
|
||||
createAction(`${dataKey}/${actionKey}`, ...args),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,7 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
export const routePath = `${getConfig().PUBLIC_PATH}:courseId`;
|
||||
|
||||
export const modalFieldKeys = StrictDict({
|
||||
adjustedGradePossible: 'adjustedGradePossible',
|
||||
|
||||
@@ -42,6 +42,37 @@ describe('app reducer', () => {
|
||||
).toEqual({ ...testingState, courseId: testValue });
|
||||
});
|
||||
});
|
||||
describe('appActions.filterMenu.startTransition', () => {
|
||||
it('sets filterMenu.transitioning to true', () => {
|
||||
expect(
|
||||
app(testingState, appActions.filterMenu.startTransition()),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
filterMenu: { ...testingState.filterMenu, transitioning: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('appActions.filterMenu.endTransition', () => {
|
||||
it('sets filterMenu.transitioning to false', () => {
|
||||
const transitioningState = {
|
||||
...testingState,
|
||||
filterMenu: { ...testingState.filterMenu, transitioning: true },
|
||||
};
|
||||
expect(
|
||||
app(transitioningState, appActions.filterMenu.endTransition()),
|
||||
).toEqual(testingState);
|
||||
});
|
||||
});
|
||||
describe('appActions.filterMenu.toggle', () => {
|
||||
it('toggles filterMenu.open', () => {
|
||||
const openState = {
|
||||
...testingState,
|
||||
filterMenu: { ...testingState.filterMenu, open: true },
|
||||
};
|
||||
expect(app(testingState, appActions.filterMenu.toggle())).toEqual(openState);
|
||||
expect(app(openState, appActions.filterMenu.toggle())).toEqual(testingState);
|
||||
});
|
||||
});
|
||||
describe('appActions.setLocalFilter', () => {
|
||||
it('loads filter values from the payload', () => {
|
||||
expect(
|
||||
|
||||
@@ -30,17 +30,13 @@ const reducer = (state = initialState, { type: actionType, payload }) => {
|
||||
assignmentGradeMax: payload.assignmentGradeMax,
|
||||
assignmentGradeMin: payload.assignmentGradeMin,
|
||||
};
|
||||
case actions.update.assignmentType.toString():
|
||||
return {
|
||||
...state,
|
||||
assignmentType: payload,
|
||||
assignment: (
|
||||
(
|
||||
payload !== ''
|
||||
&& (state.assignment || {}).type !== payload
|
||||
) ? '' : state.assignment
|
||||
),
|
||||
};
|
||||
case actions.update.assignmentType.toString(): {
|
||||
const newState = { ...state, assignmentType: payload };
|
||||
if (payload !== '' && state.assignment && payload !== state.assignment.type) {
|
||||
newState.assignment = '';
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
case actions.update.cohort.toString():
|
||||
return { ...state, cohort: payload };
|
||||
case actions.update.courseGradeLimits.toString():
|
||||
|
||||
@@ -9,6 +9,7 @@ import grades from './grades';
|
||||
import roles from './roles';
|
||||
import tracks from './tracks';
|
||||
|
||||
/* istanbul ignore next */
|
||||
const rootReducer = combineReducers({
|
||||
app,
|
||||
assignmentTypes,
|
||||
|
||||
@@ -87,6 +87,44 @@ describe('grades selectors', () => {
|
||||
describe('grade formatters', () => {
|
||||
const selectedAssignment = { assignmentId: 'block-v1:edX+type@sequential+block@abcde' };
|
||||
|
||||
describe('formatGradeOverrideForDisplay', () => {
|
||||
it('maps history entries with formatted date, grader, reason, and adjusted grade', () => {
|
||||
const historyArray = [
|
||||
{
|
||||
history_date: 'Jan 01 2021',
|
||||
history_user: 'Grog',
|
||||
override_reason: 'rage',
|
||||
earned_graded_override: 0,
|
||||
},
|
||||
{
|
||||
history_date: 'Jan 02 2021',
|
||||
history_user: 'Keyleth',
|
||||
override_reason: 'nature',
|
||||
earned_graded_override: 10,
|
||||
},
|
||||
{
|
||||
history_date: 'Jan 03 2021',
|
||||
history_user: 'Pike',
|
||||
override_reason: 'Sarenrae',
|
||||
earned_graded_override: 9001,
|
||||
},
|
||||
];
|
||||
const mapped = selectors.formatGradeOverrideForDisplay(historyArray);
|
||||
const testEntry = (index) => {
|
||||
const entry = historyArray[index];
|
||||
expect(mapped[index]).toEqual({
|
||||
date: formatDateForDisplay(new Date(entry.history_date)),
|
||||
grader: entry.history_user,
|
||||
reason: entry.override_reason,
|
||||
adjustedGrade: entry.earned_graded_override,
|
||||
});
|
||||
};
|
||||
testEntry(0);
|
||||
testEntry(1);
|
||||
testEntry(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatMinAssignmentGrade', () => {
|
||||
const modifiedGrade = '1';
|
||||
const selector = selectors.formatMinAssignmentGrade;
|
||||
|
||||
43
src/data/services/segment/constants.js
Normal file
43
src/data/services/segment/constants.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import actions from 'data/actions';
|
||||
|
||||
export const courseId = window.location.pathname.slice(1);
|
||||
|
||||
export const events = StrictDict({
|
||||
receivedRoles: 'receivedRoles',
|
||||
receivedGrades: 'receivedGrades',
|
||||
updateSucceeded: 'updateSucceeded',
|
||||
updateFailed: 'updateFailed',
|
||||
uploadOverrideSucceeded: 'uploadOverrideSucceeded',
|
||||
uploadOverrideFailed: 'uploadOverrideFailed',
|
||||
filterApplied: 'filterApplied',
|
||||
gradesReportDownloaded: 'gradesReportDownloaded',
|
||||
interventionReportDownloaded: 'interventionReportDownloaded',
|
||||
});
|
||||
|
||||
export const eventNames = StrictDict({
|
||||
[events.receivedGrades]: 'edx.gradebook.grades.displayed',
|
||||
[events.updateSucceeded]: 'edx.gradebook.grades.grade_override.succeeded',
|
||||
[events.updateFailed]: 'edx.gradebook.grades.grade_override.failed',
|
||||
[events.uploadOverrideSucceeded]: 'edx.gradebook.grades.upload.grades_overrides.succeeded',
|
||||
[events.uploadOverrideFailed]: 'edx.gradebook.grades.upload.grades_overrides.failed',
|
||||
[events.filterApplied]: 'edx.gradebook.grades.filter_applied',
|
||||
[events.gradesReportDownloaded]: 'edx.gradebook.reports.grade_export.downloaded',
|
||||
[events.interventionReportDownloaded]: 'edx.gradebook.reports.intervention.downloaded',
|
||||
});
|
||||
|
||||
export const triggers = StrictDict({
|
||||
[events.receivedRoles]: actions.roles.fetching.received.toString(),
|
||||
[events.receivedGrades]: actions.grades.fetching.received.toString(),
|
||||
[events.updateSucceeded]: actions.grades.update.success.toString(),
|
||||
[events.updateFailed]: actions.grades.update.failure.toString(),
|
||||
[events.uploadOverrideSucceeded]: actions.grades.uploadOverride.success.toString(),
|
||||
[events.uploadOverrideFailed]: actions.grades.uploadOverride.failure.toString(),
|
||||
[events.filterApplied]: actions.filters.update.courseGradeLimits.toString(),
|
||||
[events.gradesReportDownloaded]: actions.grades.downloadReport.bulkGrades.toString(),
|
||||
[events.interventionReportDownloaded]: actions.grades.downloadReport.intervention.toString(),
|
||||
});
|
||||
|
||||
export const trackingCategory = 'gradebook';
|
||||
|
||||
export const pageViewEvent = { category: trackingCategory, page: courseId };
|
||||
48
src/data/services/segment/mapping.js
Executable file
48
src/data/services/segment/mapping.js
Executable file
@@ -0,0 +1,48 @@
|
||||
import { trackPageView } from '@redux-beacon/segment';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { handleEvent } from './utils';
|
||||
|
||||
import {
|
||||
courseId,
|
||||
events,
|
||||
eventNames,
|
||||
trackingCategory,
|
||||
triggers,
|
||||
} from './constants';
|
||||
|
||||
const eventsMap = {};
|
||||
const loadTrigger = (event, options = {}) => {
|
||||
eventsMap[triggers[event]] = handleEvent(eventNames[event], options);
|
||||
};
|
||||
|
||||
eventsMap[triggers[events.receivedRoles]] = trackPageView(() => ({
|
||||
category: trackingCategory,
|
||||
page: courseId,
|
||||
}));
|
||||
loadTrigger(events.receivedGrades, {
|
||||
propsFn: (payload) => ({
|
||||
assignmentType: payload.assignmentType,
|
||||
cohort: payload.cohort,
|
||||
prev: payload.prev,
|
||||
next: payload.next,
|
||||
track: payload.track,
|
||||
}),
|
||||
});
|
||||
loadTrigger(events.updateSucceeded, {
|
||||
propsFn: ({ responseData }) => ({ updatedGrades: responseData }),
|
||||
});
|
||||
loadTrigger(events.updateFailed, {
|
||||
propsFn: ({ error }) => ({ error }),
|
||||
});
|
||||
loadTrigger(events.uploadOverrideSucceeded);
|
||||
loadTrigger(events.uploadOverrideFailed, {
|
||||
propsFn: ({ error }) => ({ error }),
|
||||
});
|
||||
loadTrigger(events.filterApplied, {
|
||||
extrasFn: () => ({ label: courseId }),
|
||||
});
|
||||
loadTrigger(events.gradesReportDownloaded);
|
||||
loadTrigger(events.interventionReportDownloaded);
|
||||
|
||||
export default StrictDict(eventsMap);
|
||||
180
src/data/services/segment/test.js
Normal file
180
src/data/services/segment/test.js
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* This module houses integration tests for the segment api integration
|
||||
*/
|
||||
import actions from 'data/actions';
|
||||
import eventsMap from './mapping';
|
||||
|
||||
import {
|
||||
courseId,
|
||||
events,
|
||||
eventNames,
|
||||
trackingCategory as category,
|
||||
triggers,
|
||||
} from './constants';
|
||||
|
||||
jest.mock('./constants', () => ({
|
||||
...jest.requireActual('./constants'),
|
||||
courseId: 'a-fake-course-id',
|
||||
}));
|
||||
jest.mock('@redux-beacon/segment', () => ({
|
||||
trackEvent: (event) => ({ trackEvent: event }),
|
||||
trackPageView: (event) => ({ trackPageView: event }),
|
||||
}));
|
||||
|
||||
describe('segments event map', () => {
|
||||
const loadEvent = (event) => ({
|
||||
event,
|
||||
trigger: triggers[event],
|
||||
handler: eventsMap[triggers[event]],
|
||||
});
|
||||
describe('app initialization (received roles api response)', () => {
|
||||
const { trigger, handler } = loadEvent(events.receivedRoles);
|
||||
test('triggers on actions.roles.fetching.received', () => {
|
||||
expect(trigger).toEqual(actions.roles.fetching.received.toString());
|
||||
});
|
||||
it('tracks the page view', () => {
|
||||
expect(handler.trackPageView()).toEqual({ category, page: courseId });
|
||||
});
|
||||
});
|
||||
describe('received grades update success', () => {
|
||||
const { event, trigger, handler } = loadEvent(events.receivedGrades);
|
||||
const payload = {
|
||||
assignmentType: 'potions',
|
||||
cohort: 3,
|
||||
prev: 'aPlace',
|
||||
next: 'aNotherPlace',
|
||||
track: 'auror',
|
||||
};
|
||||
test('triggers on actions.grades.fetching.received', () => {
|
||||
expect(trigger).toEqual(actions.grades.fetching.received.toString());
|
||||
});
|
||||
it('tracks event with ({ assignmentType, cohort, prev, next, track })', () => {
|
||||
expect(handler.trackEvent({ payload })).toEqual({
|
||||
name: eventNames[event],
|
||||
properties: {
|
||||
category,
|
||||
label: courseId,
|
||||
...payload,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('grades update success', () => {
|
||||
const { event, trigger, handler } = loadEvent(events.updateSucceeded);
|
||||
const payload = { responseData: 'some new GRADES!' };
|
||||
test('triggers on actions.grades.update.success', () => {
|
||||
expect(trigger).toEqual(actions.grades.update.success.toString());
|
||||
});
|
||||
it('tracks event with ({ updatedGrades: payload.responseData })', () => {
|
||||
expect(handler.trackEvent({ payload })).toEqual({
|
||||
name: eventNames[event],
|
||||
properties: {
|
||||
category,
|
||||
label: courseId,
|
||||
updatedGrades: payload.responseData,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('grade update failure', () => {
|
||||
const { event, trigger, handler } = loadEvent(events.updateFailed);
|
||||
const payload = { error: 'some Errors happened :-(', other: 'stuff also' };
|
||||
test('triggers on actions.grades.update.failure', () => {
|
||||
expect(trigger).toEqual(actions.grades.update.failure.toString());
|
||||
});
|
||||
it('tracks event with error info', () => {
|
||||
expect(handler.trackEvent({ payload })).toEqual({
|
||||
name: eventNames[event],
|
||||
properties: {
|
||||
category,
|
||||
label: courseId,
|
||||
error: payload.error,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('grade override upload failure', () => {
|
||||
const { event, trigger, handler } = loadEvent(events.uploadOverrideSucceeded);
|
||||
test('triggers on actions.grades.uploadOverride.success', () => {
|
||||
expect(trigger).toEqual(actions.grades.uploadOverride.success.toString());
|
||||
});
|
||||
it('tracks event', () => {
|
||||
expect(handler.trackEvent()).toEqual({
|
||||
name: eventNames[event],
|
||||
properties: {
|
||||
category,
|
||||
label: courseId,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('grade override upload failure', () => {
|
||||
const { event, trigger, handler } = loadEvent(events.uploadOverrideFailed);
|
||||
const payload = { error: 'some Errors happened :-(', other: 'stuff also' };
|
||||
test('triggers on actions.grades.uploadOverride.failure', () => {
|
||||
expect(trigger).toEqual(actions.grades.uploadOverride.failure.toString());
|
||||
});
|
||||
it('tracks event with error info', () => {
|
||||
expect(handler.trackEvent({ payload })).toEqual({
|
||||
name: eventNames[event],
|
||||
properties: {
|
||||
category,
|
||||
label: courseId,
|
||||
error: payload.error,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('gradebook filter applied', () => {
|
||||
const { event, trigger, handler } = loadEvent(events.filterApplied);
|
||||
test('triggers on actions.filters.update.courseGradeLimits', () => {
|
||||
expect(trigger).toEqual(actions.filters.update.courseGradeLimits.toString());
|
||||
});
|
||||
it('tracks event with error info', () => {
|
||||
expect(handler.trackEvent()).toEqual({
|
||||
name: eventNames[event],
|
||||
label: courseId,
|
||||
properties: {
|
||||
category,
|
||||
label: courseId,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulk grade report downloaded', () => {
|
||||
const { event, trigger, handler } = loadEvent(events.gradesReportDownloaded);
|
||||
test('triggers on actions.grades.downloadReport.bulkGrades', () => {
|
||||
expect(trigger).toEqual(actions.grades.downloadReport.bulkGrades.toString());
|
||||
});
|
||||
it('tracks event', () => {
|
||||
expect(handler.trackEvent()).toEqual({
|
||||
name: eventNames[event],
|
||||
properties: {
|
||||
category,
|
||||
label: courseId,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('intervention report downloaded', () => {
|
||||
const { event, trigger, handler } = loadEvent(events.interventionReportDownloaded);
|
||||
test('triggers on actions.grades.downloadReport.intervention', () => {
|
||||
expect(trigger).toEqual(actions.grades.downloadReport.intervention.toString());
|
||||
});
|
||||
it('tracks event', () => {
|
||||
expect(handler.trackEvent()).toEqual({
|
||||
name: eventNames[event],
|
||||
properties: {
|
||||
category,
|
||||
label: courseId,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
19
src/data/services/segment/utils.js
Executable file
19
src/data/services/segment/utils.js
Executable file
@@ -0,0 +1,19 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { trackEvent } from '@redux-beacon/segment';
|
||||
import { courseId, trackingCategory as category } from './constants';
|
||||
|
||||
export const handleEvent = (name, options = {}) => trackEvent(
|
||||
(event = {}) => {
|
||||
const { payload } = event;
|
||||
const { propsFn, extrasFn } = options;
|
||||
return {
|
||||
name,
|
||||
...(extrasFn && extrasFn(payload)),
|
||||
properties: {
|
||||
category,
|
||||
label: courseId,
|
||||
...(propsFn && propsFn(payload)),
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
59
src/data/services/segment/utils.test.js
Normal file
59
src/data/services/segment/utils.test.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as constants from './constants';
|
||||
import { handleEvent } from './utils';
|
||||
|
||||
jest.mock('@redux-beacon/segment', () => ({
|
||||
trackEvent: (handleFn) => ({ trackEvent: handleFn }),
|
||||
}));
|
||||
|
||||
const courseId = 'a-fake-course-id';
|
||||
const category = 'AFakeCategory';
|
||||
describe('segment service utils', () => {
|
||||
beforeAll(() => {
|
||||
global.window = Object.create(window);
|
||||
const url = 'http://dummy.com';
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
href: `${url}/${courseId}`,
|
||||
pathname: `/${courseId}`,
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
constants.courseId = courseId;
|
||||
constants.trackingCategory = category;
|
||||
});
|
||||
|
||||
describe('handleEvent', () => {
|
||||
const name = 'aName';
|
||||
const payload = { field1: 'some data', field2: 'other data' };
|
||||
describe('when called with just a name', () => {
|
||||
it('returns a TrackEvent call with the name, courseId, and tracking category', () => {
|
||||
const handler = handleEvent(name).trackEvent;
|
||||
expect(handler(payload)).toEqual({
|
||||
name,
|
||||
properties: { category, label: courseId },
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when a propsFn is provided', () => {
|
||||
it('adds the output of propsFn(event.payload) to properties', () => {
|
||||
const propsFn = ({ field1 }) => ({ field1 });
|
||||
const handler = handleEvent(name, { propsFn }).trackEvent;
|
||||
expect(handler({ payload })).toEqual({
|
||||
name,
|
||||
properties: { category, label: courseId, field1: payload.field1 },
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when an extrasFn object is provided', () => {
|
||||
it('adds the output of extrasFn(event.payload) to top-level object', () => {
|
||||
const extrasFn = ({ field2 }) => ({ field2 });
|
||||
const handler = handleEvent(name, { extrasFn }).trackEvent;
|
||||
expect(handler({ payload })).toEqual({
|
||||
name,
|
||||
field2: payload.field2,
|
||||
properties: { category, label: courseId },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,113 +1,41 @@
|
||||
import { applyMiddleware, createStore } from 'redux';
|
||||
import * as redux from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import { createMiddleware } from 'redux-beacon';
|
||||
import Segment, { trackEvent, trackPageView } from '@redux-beacon/segment';
|
||||
import Segment from '@redux-beacon/segment';
|
||||
|
||||
import actions from './actions';
|
||||
import selectors from './selectors';
|
||||
import reducers from './reducers';
|
||||
import eventsMap from './services/segment/mapping';
|
||||
import { configuration } from '../config';
|
||||
|
||||
const loggerMiddleware = createLogger();
|
||||
const trackingCategory = 'gradebook';
|
||||
export const createStore = () => {
|
||||
const loggerMiddleware = createLogger();
|
||||
|
||||
const eventsMap = {
|
||||
[actions.roles.fetching.received.toString()]: trackPageView((action, prevState) => ({
|
||||
category: trackingCategory,
|
||||
page: selectors.app.courseId(prevState),
|
||||
})),
|
||||
[actions.grades.fetching.received.toString()]: trackEvent(({ payload }, prevState) => ({
|
||||
name: 'edx.gradebook.grades.displayed',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: selectors.app.courseId(prevState),
|
||||
track: payload.track,
|
||||
cohort: payload.cohort,
|
||||
assignmentType: payload.assignmentType,
|
||||
prev: payload.prev,
|
||||
next: payload.next,
|
||||
},
|
||||
})),
|
||||
[actions.grades.update.success.toString()]: trackEvent(({ payload }, prevState) => ({
|
||||
name: 'edx.gradebook.grades.grade_override.succeeded',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: selectors.app.courseId(prevState),
|
||||
updatedGrades: payload.responseData,
|
||||
},
|
||||
})),
|
||||
[actions.grades.update.failure.toString()]: trackEvent(({ payload }, prevState) => ({
|
||||
name: 'edx.gradebook.grades.grade_override.failed',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: selectors.app.courseId(prevState),
|
||||
error: payload.error,
|
||||
},
|
||||
})),
|
||||
[actions.grades.uploadOverride.success.toString()]: trackEvent((action, prevState) => ({
|
||||
name: 'edx.gradebook.grades.upload.grades_overrides.succeeded',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: selectors.app.courseId(prevState),
|
||||
},
|
||||
})),
|
||||
[actions.grades.uploadOverride.failure.toString()]: trackEvent(({ payload }, prevState) => ({
|
||||
name: 'edx.gradebook.grades.upload.grades_overrides.failed',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: selectors.app.courseId(prevState),
|
||||
error: payload.error,
|
||||
},
|
||||
})),
|
||||
[actions.filters.update.courseGradeLimits]: trackEvent((action, prevState) => ({
|
||||
name: 'edx.gradebook.grades.filter_applied',
|
||||
label: selectors.app.courseId(prevState),
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: selectors.app.courseId(prevState),
|
||||
},
|
||||
})),
|
||||
[actions.grades.downloadReport.bulkGrades.toString()]: trackEvent(
|
||||
(action, prevState) => ({
|
||||
name: 'edx.gradebook.reports.grade_export.downloaded',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: selectors.app.courseId(prevState),
|
||||
},
|
||||
}),
|
||||
),
|
||||
[actions.grades.downloadReport.intervention.toString()]: trackEvent(
|
||||
(action, prevState) => ({
|
||||
name: 'edx.gradebook.reports.intervention.downloaded',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: selectors.app.courseId(prevState),
|
||||
},
|
||||
}),
|
||||
),
|
||||
const middleware = [thunkMiddleware, loggerMiddleware];
|
||||
// Conditionally add the segmentMiddleware only if the SEGMENT_KEY environment variable exists.
|
||||
if (configuration.SEGMENT_KEY) {
|
||||
middleware.push(createMiddleware(eventsMap, Segment()));
|
||||
}
|
||||
const store = redux.createStore(
|
||||
reducers,
|
||||
composeWithDevTools(redux.applyMiddleware(...middleware)),
|
||||
);
|
||||
|
||||
/**
|
||||
* Dev tools for redux work
|
||||
*/
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
window.store = store;
|
||||
window.actions = actions;
|
||||
window.selectors = selectors;
|
||||
}
|
||||
|
||||
return store;
|
||||
};
|
||||
|
||||
const middleware = [thunkMiddleware, loggerMiddleware];
|
||||
// Conditionally add the segmentMiddleware only if the SEGMENT_KEY environment variable exists.
|
||||
if (configuration.SEGMENT_KEY) {
|
||||
middleware.push(createMiddleware(eventsMap, Segment()));
|
||||
}
|
||||
const store = createStore();
|
||||
|
||||
const store = createStore(
|
||||
reducers,
|
||||
composeWithDevTools(applyMiddleware(...middleware)),
|
||||
);
|
||||
|
||||
/**
|
||||
* Dev tools for redux work
|
||||
*/
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
window.store = store;
|
||||
window.actions = actions;
|
||||
window.selectors = selectors;
|
||||
}
|
||||
|
||||
export { trackingCategory };
|
||||
export default store;
|
||||
|
||||
99
src/data/store.test.js
Normal file
99
src/data/store.test.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import { applyMiddleware } from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import { createMiddleware } from 'redux-beacon';
|
||||
import Segment from '@redux-beacon/segment';
|
||||
|
||||
import actions from './actions';
|
||||
import selectors from './selectors';
|
||||
import reducers from './reducers';
|
||||
import eventsMap from './services/segment/mapping';
|
||||
import { configuration } from '../config';
|
||||
|
||||
import exportedStore, { createStore } from './store';
|
||||
|
||||
jest.mock('./reducers', () => 'REDUCER');
|
||||
jest.mock('./actions', () => 'ACTIONS');
|
||||
jest.mock('./selectors', () => 'SELECTORS');
|
||||
jest.mock('./services/segment/mapping', () => 'EVENTS MAP');
|
||||
|
||||
jest.mock('redux-logger', () => ({
|
||||
createLogger: () => 'logger',
|
||||
}));
|
||||
jest.mock('redux-thunk', () => 'thunkMiddleware');
|
||||
jest.mock('../config', () => ({
|
||||
configuration: {
|
||||
SEGMENT_KEY: 'a-fake-segment-key',
|
||||
},
|
||||
}));
|
||||
jest.mock('redux-beacon', () => ({
|
||||
createMiddleware: jest.fn((map, model) => ({ map, model })),
|
||||
}));
|
||||
jest.mock('@redux-beacon/segment', () => () => 'Segment');
|
||||
jest.mock('redux', () => ({
|
||||
applyMiddleware: (...middleware) => ({ applied: middleware }),
|
||||
createStore: (reducer, middleware) => ({ reducer, middleware }),
|
||||
}));
|
||||
jest.mock('redux-devtools-extension/logOnlyInProduction', () => ({
|
||||
composeWithDevTools: (middleware) => ({ withDevTools: middleware }),
|
||||
}));
|
||||
|
||||
describe('store aggregator module', () => {
|
||||
describe('exported store', () => {
|
||||
it('is generated by createStore', () => {
|
||||
expect(exportedStore).toEqual(createStore());
|
||||
});
|
||||
it('creates store with connected reducers', () => {
|
||||
expect(createStore().reducer).toEqual(reducers);
|
||||
});
|
||||
describe('middleware', () => {
|
||||
describe('if SEGMENT_KEY is in the configuration', () => {
|
||||
it('also adds segment middleware from eventMap', () => {
|
||||
expect(createStore().middleware).toEqual(
|
||||
composeWithDevTools(applyMiddleware(
|
||||
thunkMiddleware,
|
||||
createLogger(),
|
||||
createMiddleware(eventsMap, Segment()),
|
||||
)),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('if no SEGMENT_KEY', () => {
|
||||
const key = configuration.SEGMENT_KEY;
|
||||
beforeEach(() => {
|
||||
configuration.SEGMENT_KEY = false;
|
||||
});
|
||||
it('exports thunk and logger middleware, composed and applied with dev tools', () => {
|
||||
expect(createStore().middleware).toEqual(
|
||||
composeWithDevTools(applyMiddleware(thunkMiddleware, createLogger())),
|
||||
);
|
||||
});
|
||||
afterEach(() => {
|
||||
configuration.SEGMENT_KEY = key;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('dev exposed tools', () => {
|
||||
beforeEach(() => {
|
||||
window.store = undefined;
|
||||
window.actions = undefined;
|
||||
window.selectors = undefined;
|
||||
});
|
||||
it('exposes redux tools if in development env', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
const store = createStore();
|
||||
expect(window.store).toEqual(store);
|
||||
expect(window.actions).toEqual(actions);
|
||||
expect(window.selectors).toEqual(selectors);
|
||||
});
|
||||
it('does not expose redux tools if in production env', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
createStore();
|
||||
expect(window.store).toEqual(undefined);
|
||||
expect(window.actions).toEqual(undefined);
|
||||
expect(window.selectors).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -35,15 +35,13 @@ export const createTestFetcher = (
|
||||
) => (
|
||||
resolveFn,
|
||||
expectedActions,
|
||||
verifyFn,
|
||||
) => {
|
||||
const store = mockStore({});
|
||||
mockedMethod.mockReturnValue(new Promise(resolve => {
|
||||
resolve(new Promise(resolveFn));
|
||||
}));
|
||||
return store.dispatch(thunkAction(...args)).then(() => {
|
||||
if (onDispatch) { onDispatch(); }
|
||||
if (verifyFn) { verifyFn(); }
|
||||
onDispatch();
|
||||
if (expectedActions !== undefined) {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
}
|
||||
|
||||
@@ -13,12 +13,14 @@ describe('Redux utilities - creators', () => {
|
||||
const keys = ['a', 'b'];
|
||||
const selectors = simpleSelectorFactory(transformer, keys);
|
||||
expect(Object.keys(selectors)).toEqual(['root', ...keys]);
|
||||
expect(selectors.root(state)).toEqual(data);
|
||||
expect(selectors.a(state)).toEqual(data.a);
|
||||
expect(selectors.b(state)).toEqual(data.b);
|
||||
});
|
||||
test('given an object for keys, returns a dict w/ simple selector per key', () => {
|
||||
const selectors = simpleSelectorFactory(transformer, data);
|
||||
expect(Object.keys(selectors)).toEqual(['root', ...Object.keys(data)]);
|
||||
expect(selectors.root(state)).toEqual(data);
|
||||
expect(selectors.a(state)).toEqual(data.a);
|
||||
expect(selectors.b(state)).toEqual(data.b);
|
||||
expect(selectors.c(state)).toEqual(data.c);
|
||||
|
||||
@@ -1,43 +1,17 @@
|
||||
/* eslint-disable import/no-named-as-default */
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import {
|
||||
APP_READY,
|
||||
getConfig,
|
||||
initialize,
|
||||
subscribe,
|
||||
} from '@edx/frontend-platform';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import Footer, { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
|
||||
import GradebookPage from './containers/GradebookPage';
|
||||
import EdxHeader from './components/EdxHeader';
|
||||
import store from './data/store';
|
||||
import './App.scss';
|
||||
|
||||
const App = () => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<div>
|
||||
<EdxHeader />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route exact path={getConfig().PUBLIC_PATH.concat(':courseId')} component={GradebookPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
</div>
|
||||
</Router>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
import App from './App';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
|
||||
50
src/index.test.jsx
Normal file
50
src/index.test.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {
|
||||
APP_READY,
|
||||
initialize,
|
||||
subscribe,
|
||||
} from '@edx/frontend-platform';
|
||||
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
|
||||
import App from './App';
|
||||
import '.';
|
||||
|
||||
jest.mock('react-dom', () => ({
|
||||
render: jest.fn(),
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
APP_READY: 'app-is-ready-key',
|
||||
initialize: jest.fn(),
|
||||
subscribe: jest.fn(),
|
||||
}));
|
||||
jest.mock('@edx/frontend-component-footer', () => ({
|
||||
messages: ['some', 'messages'],
|
||||
}));
|
||||
jest.mock('./App', () => 'App');
|
||||
|
||||
describe('app registry', () => {
|
||||
let getElement;
|
||||
|
||||
beforeEach(() => {
|
||||
getElement = window.document.getElementById;
|
||||
window.document.getElementById = jest.fn(id => ({ id }));
|
||||
});
|
||||
afterAll(() => {
|
||||
window.document.getElementById = getElement;
|
||||
});
|
||||
test('subscribe is called for APP_READY, linking App to root element', () => {
|
||||
const callArgs = subscribe.mock.calls[0];
|
||||
expect(callArgs[0]).toEqual(APP_READY);
|
||||
expect(callArgs[1]()).toEqual(
|
||||
ReactDOM.render(<App />, document.getElementById('root')),
|
||||
);
|
||||
});
|
||||
test('initialize is called with footerMessages and requireAuthenticatedUser', () => {
|
||||
expect(initialize).toHaveBeenCalledWith({
|
||||
messages: [footerMessages],
|
||||
requireAuthenticatedUser: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,27 +1,8 @@
|
||||
/* eslint-disable no-console */
|
||||
import util from 'util';
|
||||
|
||||
const staticReturnOptions = [
|
||||
'dict',
|
||||
'inspect',
|
||||
Symbol.toStringTag,
|
||||
util.inspect.custom,
|
||||
Symbol.for('nodejs.util.inspect.custom'),
|
||||
];
|
||||
|
||||
const strictGet = (target, name) => {
|
||||
if (name === Symbol.toStringTag) {
|
||||
return target;
|
||||
}
|
||||
if (name === 'length') {
|
||||
return target.length;
|
||||
}
|
||||
if (staticReturnOptions.indexOf(name) >= 0) {
|
||||
return target;
|
||||
}
|
||||
if (name === Symbol.iterator) {
|
||||
return { ...target };
|
||||
}
|
||||
|
||||
if (name in target || name === '_reactFragment') {
|
||||
return target[name];
|
||||
|
||||
62
src/utils/StrictDict.test.js
Normal file
62
src/utils/StrictDict.test.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import StrictDict from './StrictDict';
|
||||
|
||||
const value1 = 'valUE1';
|
||||
const value2 = 'vALue2';
|
||||
const key1 = 'Key1';
|
||||
const key2 = 'keY2';
|
||||
|
||||
jest.spyOn(window, 'Error').mockImplementation(error => ({ stack: error }));
|
||||
|
||||
describe('StrictDict', () => {
|
||||
let consoleError;
|
||||
let consoleLog;
|
||||
let windowError;
|
||||
beforeEach(() => {
|
||||
consoleError = window.console.error;
|
||||
consoleLog = window.console.lot;
|
||||
windowError = window.Error;
|
||||
window.console.error = jest.fn();
|
||||
window.console.log = jest.fn();
|
||||
window.Error = jest.fn(error => ({ stack: error }));
|
||||
});
|
||||
afterAll(() => {
|
||||
window.console.error = consoleError;
|
||||
window.console.log = consoleLog;
|
||||
window.Error = windowError;
|
||||
});
|
||||
const rawDict = {
|
||||
[key1]: value1,
|
||||
[key2]: value2,
|
||||
};
|
||||
const dict = StrictDict(rawDict);
|
||||
it('provides key access like a normal dict object', () => {
|
||||
expect(dict[key1]).toEqual(value1);
|
||||
});
|
||||
it('allows key listing', () => {
|
||||
expect(Object.keys(dict)).toEqual([key1, key2]);
|
||||
});
|
||||
it('allows item listing', () => {
|
||||
expect(Object.values(dict)).toEqual([value1, value2]);
|
||||
});
|
||||
it('allows stringification', () => {
|
||||
expect(dict.toString()).toEqual(rawDict.toString());
|
||||
expect({ ...dict }).toEqual({ ...rawDict });
|
||||
});
|
||||
it('allows entry listing', () => {
|
||||
expect(Object.entries(dict)).toEqual(Object.entries(rawDict));
|
||||
});
|
||||
describe('missing key', () => {
|
||||
it('logs error with target, name, and error stack', () => {
|
||||
// eslint-ignore-next-line no-unused-vars
|
||||
const callBadKey = () => dict.fakeKey;
|
||||
callBadKey();
|
||||
expect(window.console.error.mock.calls).toEqual([
|
||||
[{ target: dict, name: 'fakeKey' }],
|
||||
[Error('invalid property "fakeKey"').stack],
|
||||
]);
|
||||
});
|
||||
it('returns undefined', () => {
|
||||
expect(dict.fakeKey).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user