diff --git a/src/components/InfoPopover.jsx b/src/components/InfoPopover.jsx
new file mode 100644
index 0000000..9a6e4c1
--- /dev/null
+++ b/src/components/InfoPopover.jsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import {
+ OverlayTrigger,
+ Popover,
+ Icon,
+ IconButton,
+ PopoverContent,
+} from '@edx/paragon';
+import { InfoOutline } from '@edx/paragon/icons';
+
+/**
+ *
+ */
+export const InfoPopover = ({ children }) => (
+
+ {children}
+
+ }
+ >
+
+
+);
+
+InfoPopover.defaultProps = {};
+
+InfoPopover.propTypes = {
+ children: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.node),
+ PropTypes.node,
+ ]).isRequired,
+};
+
+export default InfoPopover;
diff --git a/src/components/InfoPopover.test.jsx b/src/components/InfoPopover.test.jsx
new file mode 100644
index 0000000..02b6958
--- /dev/null
+++ b/src/components/InfoPopover.test.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import InfoPopover from './InfoPopover';
+
+jest.mock('@edx/paragon', () => ({
+ OverlayTrigger: () => 'OverlayTrigger',
+ Icon: jest.fn().mockName('Icon'),
+ IconButton: () => 'IconButton',
+ Popover: () => 'Popover',
+ PopoverContent: () => 'PopoverContent',
+}));
+
+jest.mock('@edx/paragon/icons', () => ({
+ InfoOutline: jest.fn().mockName('icons.InfoOutline'),
+}));
+
+describe('Info Popover Component', () => {
+ const child =
Children component
;
+ test('snapshot', () => {
+ expect(shallow({child})).toMatchSnapshot();
+ });
+
+ describe('Component', () => {
+ let el;
+ beforeEach(() => {
+ el = shallow({child});
+ });
+ test('Test component render', () => {
+ expect(el.length).toEqual(1);
+ expect(el.find('.criteria-help-icon').length).toEqual(1);
+ });
+ });
+});
diff --git a/src/components/__snapshots__/InfoPopover.test.jsx.snap b/src/components/__snapshots__/InfoPopover.test.jsx.snap
new file mode 100644
index 0000000..d1ae88f
--- /dev/null
+++ b/src/components/__snapshots__/InfoPopover.test.jsx.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Info Popover Component snapshot 1`] = `
+
+
+
+ Children component
+
+
+
+ }
+ placement="auto"
+ trigger="focus"
+>
+
+
+`;
diff --git a/src/containers/CriterionContainer/CriterionFeedback.jsx b/src/containers/CriterionContainer/CriterionFeedback.jsx
index 375eb77..2deb4b9 100644
--- a/src/containers/CriterionContainer/CriterionFeedback.jsx
+++ b/src/containers/CriterionContainer/CriterionFeedback.jsx
@@ -18,26 +18,27 @@ export class CriterionFeedback extends React.Component {
}
onChange(event) {
- this.props.setValue({ value: event.target.value, orderNum: this.props.orderNum });
+ this.props.setValue({
+ value: event.target.value,
+ orderNum: this.props.orderNum,
+ });
}
render() {
- if (this.props.config === feedbackRequirement.disabled) {
+ const { config, isGrading, value } = this.props;
+ if (config === feedbackRequirement.disabled) {
return null;
}
- return this.props.isGrading
- ? (
-
- )
- : (
- {this.props.value}
- );
+ return (
+
+ );
}
}
@@ -47,15 +48,14 @@ CriterionFeedback.defaultProps = {
CriterionFeedback.propTypes = {
orderNum: PropTypes.number.isRequired,
+ isGrading: PropTypes.bool.isRequired,
// redux
config: PropTypes.string.isRequired,
- isGrading: PropTypes.bool.isRequired,
setValue: PropTypes.func.isRequired,
value: PropTypes.string,
};
export const mapStateToProps = (state, { orderNum }) => ({
- isGrading: selectors.app.isGrading(state),
config: selectors.app.rubric.criterionFeedbackConfig(state, { orderNum }),
value: selectors.grading.selected.criterionFeedback(state, { orderNum }),
});
diff --git a/src/containers/CriterionContainer/CriterionFeedback.test.jsx b/src/containers/CriterionContainer/CriterionFeedback.test.jsx
new file mode 100644
index 0000000..820ebfe
--- /dev/null
+++ b/src/containers/CriterionContainer/CriterionFeedback.test.jsx
@@ -0,0 +1,142 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import actions from 'data/actions';
+import selectors from 'data/selectors';
+import {
+ CriterionFeedback,
+ mapStateToProps,
+ mapDispatchToProps,
+} from './CriterionFeedback';
+import {
+ feedbackRequirement,
+ gradeStatuses,
+} from 'data/services/lms/constants';
+
+jest.mock('@edx/paragon', () => ({
+ Form: {
+ Control: () => 'Form.Control',
+ },
+}));
+
+jest.mock('data/selectors', () => ({
+ __esModule: true,
+ default: {
+ app: {
+ rubric: {
+ criterionFeedbackConfig: jest.fn((...args) => ({
+ rubricCriterionFeedbackConfig: args,
+ })),
+ },
+ },
+ grading: {
+ selected: {
+ criterionFeedback: jest.fn((...args) => ({
+ selectedCriterionFeedback: args,
+ })),
+ gradeStatus: jest.fn((...args) => ({ selectedGradeStatus: args })),
+ },
+ },
+ },
+}));
+
+describe('Criterion Feedback', () => {
+ const props = {
+ orderNum: 1,
+ config: 'config string',
+ isGrading: true,
+ value: 'some value',
+ gradeStatus: gradeStatuses.ungraded,
+ setValue: jest.fn().mockName('this.props.setValue'),
+ };
+ let el;
+ beforeEach(() => {
+ el = shallow();
+ el.instance().onChange = jest.fn().mockName('this.onChange');
+ });
+ describe('snapshot', () => {
+ test('is grading', () => {
+ expect(el.instance().render()).toMatchSnapshot();
+ });
+
+ test('is graded', () => {
+ el.setProps({
+ isGrading: false,
+ gradeStatus: gradeStatuses.graded,
+ });
+ expect(el.instance().render()).toMatchSnapshot();
+ });
+
+ test('is configure to disabled', () => {
+ el.setProps({
+ config: feedbackRequirement.disabled,
+ });
+ expect(el.instance().render()).toMatchSnapshot();
+ });
+ });
+
+ describe('component', () => {
+ describe('render', () => {
+ test('is grading (the feedback input is not disabled)', () => {
+ expect(el.isEmptyRender()).toEqual(false);
+ expect(el.prop('value')).toEqual(props.value);
+ expect(el.prop('disabled')).toEqual(false);
+ });
+ test('is graded (the input is disabled)', () => {
+ el.setProps({
+ isGrading: false,
+ gradeStatus: gradeStatuses.graded,
+ });
+ expect(el.prop('value')).toEqual(props.value);
+ expect(el.prop('disabled')).toEqual(true);
+ });
+ test('is configure to disabled (the input does not get render)', () => {
+ el.setProps({
+ config: feedbackRequirement.disabled,
+ });
+ expect(el.isEmptyRender()).toEqual(true);
+ });
+ });
+
+ describe('behavior', () => {
+ test('onChange call set value', () => {
+ el = shallow();
+ el.instance().onChange({
+ target: {
+ value: 'some value',
+ },
+ });
+ expect(props.setValue).toBeCalledTimes(1);
+ });
+ });
+ });
+
+ describe('mapStateToProps', () => {
+ const testState = { abitaryState: 'some data' };
+ const ownProps = { orderNum: props.orderNum };
+ let mapped;
+ beforeEach(() => {
+ mapped = mapStateToProps(testState, ownProps);
+ });
+
+ test('selectors.app.rubric.criterionFeedbackConfig', () => {
+ expect(mapped.config).toEqual(
+ selectors.app.rubric.criterionFeedbackConfig(testState, ownProps),
+ );
+ });
+
+ test('selector.grading.selected.criterionFeedback', () => {
+ expect(mapped.value).toEqual(
+ selectors.grading.selected.criterionFeedback(testState, ownProps),
+ );
+ });
+ });
+
+ describe('mapDispatchToProps', () => {
+ test('maps actions.grading.setCriterionFeedback to setValue prop', () => {
+ expect(mapDispatchToProps.setValue).toEqual(
+ actions.grading.setCriterionFeedback,
+ );
+ });
+ });
+});
diff --git a/src/containers/CriterionContainer/OptionInfoPopover.jsx b/src/containers/CriterionContainer/OptionInfoPopover.jsx
deleted file mode 100644
index e23694f..0000000
--- a/src/containers/CriterionContainer/OptionInfoPopover.jsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import {
- OverlayTrigger,
- Popover,
- Icon,
- IconButton,
-} from '@edx/paragon';
-import { InfoOutline } from '@edx/paragon/icons';
-
-/**
- *
- */
-export const OptionInfoPopover = ({ options }) => (
-
-
- {options.map(option => (
-
- {option.label}
- {option.explanation}
-
- ))}
-
-
- )}
- >
- {}}
- src={InfoOutline}
- alt="criterion info"
- iconAs={Icon}
- />
-
-);
-
-OptionInfoPopover.defaultProps = {
-};
-
-OptionInfoPopover.propTypes = {
- // redux
- options: PropTypes.arrayOf(PropTypes.shape({
- explanation: PropTypes.string,
- label: PropTypes.string,
- name: PropTypes.string,
- points: PropTypes.number,
- })).isRequired,
-};
-
-export default OptionInfoPopover;
diff --git a/src/containers/CriterionContainer/GradingCriterion.jsx b/src/containers/CriterionContainer/RadioCriterion.jsx
similarity index 68%
rename from src/containers/CriterionContainer/GradingCriterion.jsx
rename to src/containers/CriterionContainer/RadioCriterion.jsx
index 70590a2..cea8c33 100644
--- a/src/containers/CriterionContainer/GradingCriterion.jsx
+++ b/src/containers/CriterionContainer/RadioCriterion.jsx
@@ -8,9 +8,9 @@ import actions from 'data/actions';
import selectors from 'data/selectors';
/**
- *
+ *
*/
-export class GradingCriterion extends React.Component {
+export class RadioCriterion extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
@@ -24,51 +24,52 @@ export class GradingCriterion extends React.Component {
}
render() {
- const { config, data } = this.props;
+ const { config, data, isGrading } = this.props;
return (
<>
-
- { config.options.map(option => (
+
+ {config.options.map((option) => (
{option.label}
- )) }
+ ))}
>
);
}
}
-GradingCriterion.defaultProps = {
+RadioCriterion.defaultProps = {
data: {
selectedOption: '',
feedback: '',
},
};
-GradingCriterion.propTypes = {
+RadioCriterion.propTypes = {
orderNum: PropTypes.number.isRequired,
+ isGrading: PropTypes.bool.isRequired,
// redux
config: PropTypes.shape({
prompt: PropTypes.string,
name: PropTypes.string,
feedback: PropTypes.string,
- options: PropTypes.arrayOf(PropTypes.shape({
- explanation: PropTypes.string,
- feedback: PropTypes.string,
- label: PropTypes.string,
- name: PropTypes.string,
- points: PropTypes.number,
- })),
+ options: PropTypes.arrayOf(
+ PropTypes.shape({
+ explanation: PropTypes.string,
+ feedback: PropTypes.string,
+ label: PropTypes.string,
+ name: PropTypes.string,
+ points: PropTypes.number,
+ }),
+ ),
}).isRequired,
data: PropTypes.shape({
selectedOption: PropTypes.string,
@@ -86,4 +87,4 @@ export const mapDispatchToProps = {
setCriterionOption: actions.grading.setCriterionOption,
};
-export default connect(mapStateToProps, mapDispatchToProps)(GradingCriterion);
+export default connect(mapStateToProps, mapDispatchToProps)(RadioCriterion);
diff --git a/src/containers/CriterionContainer/RadioCriterion.test.jsx b/src/containers/CriterionContainer/RadioCriterion.test.jsx
new file mode 100644
index 0000000..de6ae13
--- /dev/null
+++ b/src/containers/CriterionContainer/RadioCriterion.test.jsx
@@ -0,0 +1,153 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import actions from 'data/actions';
+import selectors from 'data/selectors';
+import {
+ RadioCriterion,
+ mapDispatchToProps,
+ mapStateToProps,
+} from './RadioCriterion';
+
+jest.mock('@edx/paragon', () => ({
+ Form: {
+ RadioSet: () => 'Form.RadioSet',
+ Radio: () => 'Form.Radio',
+ },
+}));
+
+jest.mock('data/selectors', () => ({
+ __esModule: true,
+ default: {
+ app: {
+ rubric: {
+ criterionConfig: jest.fn((...args) => ({
+ rubricCriterionConfig: args,
+ })),
+ },
+ },
+ grading: {
+ selected: {
+ criterionGradeData: jest.fn((...args) => ({
+ selectedCriterionGradeData: args,
+ })),
+ },
+ },
+ },
+}));
+
+describe('Radio Crition Container', () => {
+ const props = {
+ orderNum: 1,
+ isGrading: true,
+ config: {
+ prompt: 'prompt',
+ name: 'random name',
+ feedback: 'feedback mock',
+ options: [
+ {
+ explanation: 'explaination',
+ feedback: 'option feedback',
+ label: 'this label',
+ name: 'option name',
+ points: 1,
+ },
+ {
+ explanation: 'explaination 2',
+ feedback: 'option feedback 2',
+ label: 'this label 2',
+ name: 'option name 2',
+ points: 2,
+ },
+ ],
+ },
+ data: {
+ selectedOption: 'selected option',
+ feedback: 'data feedback',
+ },
+ setCriterionOption: jest.fn().mockName('this.props.setCriterionOption'),
+ };
+
+ let el;
+ beforeEach(() => {
+ el = shallow();
+ el.instance().onChange = jest.fn().mockName('this.onChange');
+ });
+ describe('snapshot', () => {
+ test('is grading', () => {
+ expect(el.instance().render()).toMatchSnapshot();
+ });
+
+ test('is not grading', () => {
+ el.setProps({
+ isGrading: false,
+ });
+ expect(el.instance().render()).toMatchSnapshot();
+ });
+ });
+
+ describe('component', () => {
+ describe('rendering', () => {
+ test('is grading (all options are not disabled)', () => {
+ expect(el.isEmptyRender()).toEqual(false);
+ const optionsEl = el.find('.criteria-option');
+ expect(optionsEl.length).toEqual(props.config.options.length);
+ optionsEl.forEach((optionEl) =>
+ expect(optionEl.prop('disabled')).toEqual(false),
+ );
+ });
+
+ test('is not grading (all options are disabled)', () => {
+ el.setProps({
+ isGrading: false,
+ });
+ expect(el.isEmptyRender()).toEqual(false);
+ const optionsEl = el.find('.criteria-option');
+ expect(optionsEl.length).toEqual(props.config.options.length);
+ optionsEl.forEach((optionEl) =>
+ expect(optionEl.prop('disabled')).toEqual(true),
+ );
+ });
+ });
+
+ describe('behavior', () => {
+ test('onChange call set crition option', () => {
+ el = shallow();
+ el.instance().onChange({
+ target: {
+ value: 'some value',
+ },
+ });
+ expect(props.setCriterionOption).toBeCalledTimes(1);
+ });
+ });
+ });
+
+ describe('mapStateToProps', () => {
+ const testState = { arbitary: 'some data' };
+ const ownProps = { orderNum: props.orderNum };
+ let mapped;
+ beforeEach(() => {
+ mapped = mapStateToProps(testState, ownProps);
+ });
+ test('selectors.app.rubric.criterionConfig', () => {
+ expect(mapped.config).toEqual(
+ selectors.app.rubric.criterionConfig(testState, ownProps),
+ );
+ });
+
+ test('selectors.grading.selected.criterionGradeData', () => {
+ expect(mapped.data).toEqual(
+ selectors.grading.selected.criterionGradeData(testState, ownProps),
+ );
+ });
+ });
+
+ describe('mapDispatchToProps', () => {
+ test('maps actions.grading.setCriterionFeedback to setValue prop', () => {
+ expect(mapDispatchToProps.setCriterionOption).toEqual(
+ actions.grading.setCriterionOption,
+ );
+ });
+ });
+});
diff --git a/src/containers/CriterionContainer/ReviewCriterion.jsx b/src/containers/CriterionContainer/ReviewCriterion.jsx
index 4fd3839..5721867 100644
--- a/src/containers/CriterionContainer/ReviewCriterion.jsx
+++ b/src/containers/CriterionContainer/ReviewCriterion.jsx
@@ -2,33 +2,25 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import {
- Form,
- FormControlFeedback,
-} from '@edx/paragon';
+import { Form, FormControlFeedback } from '@edx/paragon';
import selectors from 'data/selectors';
/**
*
*/
-export const ReviewCriterion = ({
- config,
- // data,
-}) => (
+export const ReviewCriterion = ({ config }) => (
- { config.options.map(option => (
+ {config.options.map((option) => (
-
- {option.label}
-
+ {option.label}
{`${option.points} points`}
- )) }
+ ))}
);
@@ -40,12 +32,14 @@ ReviewCriterion.propTypes = {
config: PropTypes.shape({
prompt: PropTypes.string,
feedback: PropTypes.string,
- options: PropTypes.arrayOf(PropTypes.shape({
- explanation: PropTypes.string,
- label: PropTypes.string,
- name: PropTypes.string,
- points: PropTypes.number,
- })),
+ options: PropTypes.arrayOf(
+ PropTypes.shape({
+ explanation: PropTypes.string,
+ label: PropTypes.string,
+ name: PropTypes.string,
+ points: PropTypes.number,
+ }),
+ ),
}).isRequired,
data: PropTypes.shape({
selectedOption: PropTypes.string,
@@ -58,7 +52,6 @@ export const mapStateToProps = (state, { orderNum }) => ({
data: selectors.grading.selected.criterionGradeData(state, { orderNum }),
});
-export const mapDispatchToProps = {
-};
+export const mapDispatchToProps = {};
export default connect(mapStateToProps, mapDispatchToProps)(ReviewCriterion);
diff --git a/src/containers/CriterionContainer/ReviewCriterion.test.jsx b/src/containers/CriterionContainer/ReviewCriterion.test.jsx
new file mode 100644
index 0000000..171b713
--- /dev/null
+++ b/src/containers/CriterionContainer/ReviewCriterion.test.jsx
@@ -0,0 +1,110 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import actions from 'data/actions';
+import selectors from 'data/selectors';
+import { ReviewCriterion, mapStateToProps } from './ReviewCriterion';
+
+jest.mock('@edx/paragon', () => ({
+ Form: {
+ Label: () => 'Form.Label',
+ },
+ FormControlFeedback: () => 'FormControlFeedback',
+}));
+
+jest.mock('data/selectors', () => ({
+ __esModule: true,
+ default: {
+ app: {
+ rubric: {
+ criterionConfig: jest.fn((...args) => ({
+ rubricCriterionConfig: args,
+ })),
+ },
+ },
+ grading: {
+ selected: {
+ criterionGradeData: jest.fn((...args) => ({
+ selectedCriterionGradeData: args,
+ })),
+ },
+ },
+ },
+}));
+
+describe('Review Crition Container', () => {
+ const props = {
+ orderNum: 1,
+ config: {
+ prompt: 'prompt',
+ name: 'random name',
+ feedback: 'feedback mock',
+ options: [
+ {
+ explanation: 'explaination',
+ feedback: 'option feedback',
+ label: 'this label',
+ name: 'option name',
+ points: 1,
+ },
+ {
+ explanation: 'explaination 2',
+ feedback: 'option feedback 2',
+ label: 'this label 2',
+ name: 'option name 2',
+ points: 2,
+ },
+ ],
+ },
+ data: {
+ selectedOption: 'selected option',
+ feedback: 'data feedback',
+ },
+ };
+
+ let el;
+ beforeEach(() => {
+ el = shallow();
+ });
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ });
+
+ describe('component', () => {
+ test('rendering (everything show up)', () => {
+ expect(el.isEmptyRender()).toEqual(false);
+ const optionsEl = el.find('.criteria-option');
+ expect(optionsEl.length).toEqual(props.config.options.length);
+ optionsEl.forEach((optionEl, i) => {
+ let option = props.config.options[i];
+ expect(optionEl.key()).toEqual(option.name);
+ expect(optionEl.find('.option-label').childAt(0).text()).toEqual(
+ option.label,
+ );
+ expect(optionEl.find('.option-points').childAt(0).text()).toContain(
+ String(option.points),
+ );
+ });
+ });
+ });
+
+ describe('mapStateToProps', () => {
+ const testState = { arbitary: 'some data' };
+ const ownProps = { orderNum: props.orderNum };
+ let mapped;
+ beforeEach(() => {
+ mapped = mapStateToProps(testState, ownProps);
+ });
+ test('selectors.app.rubric.criterionConfig', () => {
+ expect(mapped.config).toEqual(
+ selectors.app.rubric.criterionConfig(testState, ownProps),
+ );
+ });
+
+ test('selectors.grading.selected.criterionGradeData', () => {
+ expect(mapped.data).toEqual(
+ selectors.grading.selected.criterionGradeData(testState, ownProps),
+ );
+ });
+ });
+});
diff --git a/src/containers/CriterionContainer/__snapshots__/CriterionFeedback.test.jsx.snap b/src/containers/CriterionContainer/__snapshots__/CriterionFeedback.test.jsx.snap
new file mode 100644
index 0000000..74bc88d
--- /dev/null
+++ b/src/containers/CriterionContainer/__snapshots__/CriterionFeedback.test.jsx.snap
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Criterion Feedback snapshot is configure to disabled 1`] = `null`;
+
+exports[`Criterion Feedback snapshot is graded 1`] = `
+
+`;
+
+exports[`Criterion Feedback snapshot is grading 1`] = `
+
+`;
diff --git a/src/containers/CriterionContainer/__snapshots__/RadioCriterion.test.jsx.snap b/src/containers/CriterionContainer/__snapshots__/RadioCriterion.test.jsx.snap
new file mode 100644
index 0000000..eae402b
--- /dev/null
+++ b/src/containers/CriterionContainer/__snapshots__/RadioCriterion.test.jsx.snap
@@ -0,0 +1,57 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Radio Crition Container snapshot is grading 1`] = `
+
+
+
+ this label
+
+
+ this label 2
+
+
+
+`;
+
+exports[`Radio Crition Container snapshot is not grading 1`] = `
+
+
+
+ this label
+
+
+ this label 2
+
+
+
+`;
diff --git a/src/containers/CriterionContainer/__snapshots__/ReviewCriterion.test.jsx.snap b/src/containers/CriterionContainer/__snapshots__/ReviewCriterion.test.jsx.snap
new file mode 100644
index 0000000..3ea05db
--- /dev/null
+++ b/src/containers/CriterionContainer/__snapshots__/ReviewCriterion.test.jsx.snap
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Review Crition Container snapshot 1`] = `
+
+
+
+
+
+ 1 points
+
+
+
+
+
+
+
+ 2 points
+
+
+
+
+`;
diff --git a/src/containers/CriterionContainer/__snapshots__/index.test.jsx.snap b/src/containers/CriterionContainer/__snapshots__/index.test.jsx.snap
new file mode 100644
index 0000000..fce0421
--- /dev/null
+++ b/src/containers/CriterionContainer/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,144 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Criterion Container snapshot is graded and is not grading 1`] = `
+
+
+
+
+
+
+
+`;
+
+exports[`Criterion Container snapshot is ungraded and is grading 1`] = `
+
+
+
+
+
+
+
+`;
+
+exports[`Criterion Container snapshot is ungraded and is not grading 1`] = `
+
+
+
+
+
+
+
+`;
diff --git a/src/containers/CriterionContainer/index.jsx b/src/containers/CriterionContainer/index.jsx
index 4593c2b..ad29114 100644
--- a/src/containers/CriterionContainer/index.jsx
+++ b/src/containers/CriterionContainer/index.jsx
@@ -4,56 +4,48 @@ import { connect } from 'react-redux';
import { Form } from '@edx/paragon';
-import actions from 'data/actions';
import selectors from 'data/selectors';
-import GradingCriterion from './GradingCriterion';
-import ReviewCriterion from './ReviewCriterion';
+import InfoPopover from 'components/InfoPopover';
+import RadioCriterion from './RadioCriterion';
import CriterionFeedback from './CriterionFeedback';
-import OptionInfoPopover from './OptionInfoPopover';
+import ReviewCriterion from './ReviewCriterion';
+import { gradeStatuses } from 'data/services/lms/constants';
/**
*
*/
export class CriterionContainer extends React.Component {
- constructor(props) {
- super(props);
- this.handleFeedbackUpdate = this.handleFeedbackUpdate.bind(this);
- }
-
- handleFeedbackUpdate(event) {
- this.props.setFeedback({ orderNum: this.props.orderNum, value: event.target.value });
- }
-
render() {
- const {
- config,
- isGrading,
- orderNum,
- } = this.props;
+ const { config, isGrading, orderNum, gradeStatus } = this.props;
return (
-
- {config.prompt}
-
-
+ {config.prompt}
+
+ {config.options.map((option) => (
+
+ {option.label}
+
+ {option.explanation}
+
+ ))}
+
- {
- isGrading
- ?
- :
- }
+ {isGrading || gradeStatus === gradeStatuses.graded ? (
+
+ ) : (
+
+ )}
-
+
);
}
}
-CriterionContainer.defaultProps = {
-};
+CriterionContainer.defaultProps = {};
CriterionContainer.propTypes = {
isGrading: PropTypes.bool.isRequired,
@@ -62,22 +54,23 @@ CriterionContainer.propTypes = {
config: PropTypes.shape({
prompt: PropTypes.string,
feedback: PropTypes.string,
- options: PropTypes.arrayOf(PropTypes.shape({
- explanation: PropTypes.string,
- label: PropTypes.string,
- name: PropTypes.string,
- points: PropTypes.number,
- })),
+ options: PropTypes.arrayOf(
+ PropTypes.shape({
+ explanation: PropTypes.string,
+ label: PropTypes.string,
+ name: PropTypes.string,
+ points: PropTypes.number,
+ }),
+ ),
}).isRequired,
- setFeedback: PropTypes.func.isRequired,
+ gradeStatus: PropTypes.oneOf(Object.values(gradeStatuses)).isRequired,
};
export const mapStateToProps = (state, { orderNum }) => ({
config: selectors.app.rubric.criterionConfig(state, { orderNum }),
+ gradeStatus: selectors.grading.selected.gradeStatus(state),
});
-export const mapDispatchToProps = {
- setFeedback: actions.grading.setCriterionFeedback,
-};
+export const mapDispatchToProps = {};
export default connect(mapStateToProps, mapDispatchToProps)(CriterionContainer);
diff --git a/src/containers/CriterionContainer/index.test.jsx b/src/containers/CriterionContainer/index.test.jsx
new file mode 100644
index 0000000..d5cc0e9
--- /dev/null
+++ b/src/containers/CriterionContainer/index.test.jsx
@@ -0,0 +1,144 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import selectors from 'data/selectors';
+import { CriterionContainer, mapStateToProps } from '.';
+import { gradeStatuses } from 'data/services/lms/constants';
+
+jest.mock('components/InfoPopover', () => 'InfoPopover');
+jest.mock('./RadioCriterion', () => 'RadioCriterion');
+jest.mock('./CriterionFeedback', () => 'CriterionFeedback');
+jest.mock('./ReviewCriterion', () => 'ReviewCriterion');
+
+jest.mock('@edx/paragon', () => ({
+ Form: {
+ Group: () => 'Form.Group',
+ Label: () => 'Form.Label',
+ },
+}));
+
+jest.mock('data/selectors', () => ({
+ __esModule: true,
+ default: {
+ app: {
+ rubric: {
+ criterionConfig: jest.fn((...args) => ({
+ rubricCriterionConfig: args,
+ })),
+ },
+ },
+ grading: {
+ selected: {
+ gradeStatus: jest.fn((...args) => ({ selectedGradeStatus: args })),
+ },
+ },
+ },
+}));
+
+describe('Criterion Container', () => {
+ const props = {
+ isGrading: true,
+ orderNum: 1,
+ config: {
+ prompt: 'prompt',
+ name: 'random name',
+ feedback: 'feedback mock',
+ options: [
+ {
+ explanation: 'explaination',
+ feedback: 'option feedback',
+ label: 'this label',
+ name: 'option name',
+ points: 2,
+ },
+ {
+ explanation: 'explaination 2',
+ feedback: 'option feedback 2',
+ label: 'this label 2',
+ name: 'option name 2',
+ points: 1,
+ },
+ ],
+ },
+ gradeStatus: gradeStatuses.ungraded,
+ };
+ let el;
+ beforeEach(() => {
+ el = shallow();
+ });
+
+ describe('snapshot', () => {
+ test('is ungraded and is grading', () => {
+ expect(el).toMatchSnapshot();
+ });
+
+ test('is ungraded and is not grading', () => {
+ el.setProps({
+ isGrading: false,
+ });
+ expect(el).toMatchSnapshot();
+ });
+
+ test('is graded and is not grading', () => {
+ el.setProps({
+ isGrading: false,
+ gradeStatus: gradeStatuses.graded,
+ });
+ expect(el).toMatchSnapshot();
+ });
+ });
+
+ describe('component', () => {
+ test('rendering and all of the option show up', () => {
+ expect(el.isEmptyRender()).toEqual(false);
+ const optionsEl = el.find('.help-popover-option');
+ expect(optionsEl.length).toEqual(props.config.options.length);
+ optionsEl.forEach((optionEl, i) => {
+ expect(optionEl.key()).toEqual(props.config.options[i].name);
+ expect(optionEl.text()).toContain(props.config.options[i].explanation);
+ });
+ });
+
+ test('is ungraded and is grading (Radio criterion get render)', () => {
+ const rubricCritera = el.find('.rubric-criteria');
+ expect(rubricCritera.children(0).name()).toEqual('RadioCriterion');
+ });
+
+ test('is ungraded and is not grading (Review criterion get render)', () => {
+ el.setProps({
+ isGrading: false,
+ });
+ const rubricCritera = el.find('.rubric-criteria');
+ expect(rubricCritera.children(0).name()).toEqual('ReviewCriterion');
+ });
+
+ test('is graded and is not grading (Radio criterion get render)', () => {
+ el.setProps({
+ isGrading: false,
+ gradeStatus: gradeStatuses.graded,
+ });
+ const rubricCritera = el.find('.rubric-criteria');
+ expect(rubricCritera.children(0).name()).toEqual('RadioCriterion');
+ });
+ });
+
+ describe('mapStateToProps', () => {
+ const testState = { abitaryState: 'some data' };
+ const ownProps = { orderNum: props.orderNum };
+ let mapped;
+ beforeEach(() => {
+ mapped = mapStateToProps(testState, ownProps);
+ });
+ test('selectors.app.rubric.criterionConfig', () => {
+ expect(mapped.config).toEqual(
+ selectors.app.rubric.criterionConfig(testState, ownProps),
+ );
+ });
+
+ test('selectors.grading.selected.gradeStatus', () => {
+ expect(mapped.gradeStatus).toEqual(
+ selectors.grading.selected.gradeStatus(testState),
+ );
+ });
+ });
+});
diff --git a/src/containers/Rubric/RubricFeedback.jsx b/src/containers/Rubric/RubricFeedback.jsx
index e87d372..e4cd57f 100644
--- a/src/containers/Rubric/RubricFeedback.jsx
+++ b/src/containers/Rubric/RubricFeedback.jsx
@@ -7,6 +7,7 @@ import { Form } from '@edx/paragon';
import { feedbackRequirement } from 'data/services/lms/constants';
import actions from 'data/actions';
import selectors from 'data/selectors';
+import InfoPopover from 'components/InfoPopover';
/**
*
@@ -22,22 +23,28 @@ export class RubricFeedback extends React.Component {
}
render() {
- if (this.props.config === feedbackRequirement.disabled) {
+ const { isGrading, value, feedbackPrompt, config } = this.props;
+ if (config === feedbackRequirement.disabled) {
return null;
}
- return this.props.isGrading
- ? (
+ return (
+
+
+ Overall comments
+
+ {feedbackPrompt}
+
+
- )
- : (
- {this.props.value}
- );
+
+ );
}
}
@@ -51,12 +58,14 @@ RubricFeedback.propTypes = {
isGrading: PropTypes.bool.isRequired,
setValue: PropTypes.func.isRequired,
value: PropTypes.string,
+ feedbackPrompt: PropTypes.string.isRequired,
};
export const mapStateToProps = (state) => ({
isGrading: selectors.app.isGrading(state),
value: selectors.grading.selected.overallFeedback(state),
config: selectors.app.rubric.feedbackConfig(state),
+ feedbackPrompt: selectors.app.rubric.feedbackPrompt(state),
});
export const mapDispatchToProps = {
diff --git a/src/containers/Rubric/RubricFeedback.test.jsx b/src/containers/Rubric/RubricFeedback.test.jsx
new file mode 100644
index 0000000..3dadadc
--- /dev/null
+++ b/src/containers/Rubric/RubricFeedback.test.jsx
@@ -0,0 +1,160 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import actions from 'data/actions';
+import selectors from 'data/selectors';
+import {
+ RubricFeedback,
+ mapDispatchToProps,
+ mapStateToProps,
+} from './RubricFeedback';
+import {
+ feedbackRequirement,
+ gradeStatuses,
+} from 'data/services/lms/constants';
+
+jest.mock('components/InfoPopover', () => 'InfoPopover');
+
+jest.mock('@edx/paragon', () => ({
+ Form: {
+ Group: () => 'Form.Group',
+ Label: () => 'Form.Label',
+ Control: () => 'Form.Control',
+ },
+}));
+
+jest.mock('data/selectors', () => ({
+ __esModule: true,
+ default: {
+ app: {
+ isGrading: jest.fn((...args) => ({ isGragrding: args })),
+ rubric: {
+ feedbackConfig: jest.fn((...args) => ({
+ rubricFeedbackConfig: args,
+ })),
+ feedbackPrompt: jest.fn((...args) => ({
+ rubricFeedbackPrompt: args,
+ })),
+ },
+ },
+ grading: {
+ selected: {
+ overallFeedback: jest.fn((...args) => ({
+ selectedOverallFeedback: args,
+ })),
+ },
+ },
+ },
+}));
+
+describe('Review Feedback component', () => {
+ const props = {
+ config: 'config stirng',
+ isGrading: true,
+ value: 'some value',
+ feedbackPrompt: 'feedback prompt',
+ gradeStatus: gradeStatuses.ungraded,
+ setValue: jest.fn().mockName('this.props.setValue'),
+ };
+
+ let el;
+ beforeEach(() => {
+ el = shallow();
+ el.instance().onChange = jest.fn().mockName('this.onChange');
+ });
+ describe('snapshot', () => {
+ test('is grading', () => {
+ expect(el.instance().render()).toMatchSnapshot();
+ });
+ test('is graded', () => {
+ el.setProps({
+ isGrading: false,
+ gradeStatus: gradeStatuses.graded,
+ });
+ expect(el.instance().render()).toMatchSnapshot();
+ });
+
+ test('is configure to disabled', () => {
+ el.setProps({
+ config: feedbackRequirement.disabled,
+ });
+ expect(el.instance().render()).toMatchSnapshot();
+ });
+ });
+
+ describe('component', () => {
+ describe('render', () => {
+ test('is grading (everything show up and the input is editable)', () => {
+ expect(el.isEmptyRender()).toEqual(false);
+ const input = el.find('.rubric-feedback.feedback-input');
+ expect(input.prop('disabled')).toEqual(false);
+ expect(input.prop('value')).toEqual(props.value);
+ });
+
+ test('is graded (the input are disabled)', () => {
+ el.setProps({
+ isGrading: false,
+ gradeStatus: gradeStatuses.graded,
+ });
+ expect(el.isEmptyRender()).toEqual(false);
+ const input = el.find('.rubric-feedback.feedback-input');
+ expect(input.prop('disabled')).toEqual(true);
+ expect(input.prop('value')).toEqual(props.value);
+ });
+ test('is configure to disabled (this input does not get render)', () => {
+ el.setProps({
+ config: feedbackRequirement.disabled,
+ });
+ expect(el.isEmptyRender()).toEqual(true);
+ });
+ });
+ describe('behavior', () => {
+ test('onChange set value', () => {
+ el = shallow();
+ el.instance().onChange({
+ target: {
+ value: 'some value',
+ },
+ });
+ expect(props.setValue).toBeCalledTimes(1);
+ });
+ });
+ });
+
+ describe('mapStateToProps', () => {
+ const testState = { abitaryState: 'some data' };
+ let mapped;
+ beforeEach(() => {
+ mapped = mapStateToProps(testState);
+ });
+ test('selectors.app.isGrading', () => {
+ expect(mapped.isGrading).toEqual(selectors.app.isGrading(testState));
+ });
+
+ test('selectors.app.rubricFeedbackConfig', () => {
+ expect(mapped.config).toEqual(
+ selectors.app.rubric.feedbackConfig(testState),
+ );
+ });
+
+ test('selectors.grading.selected.overallFeedback', () => {
+ expect(mapped.value).toEqual(
+ selectors.grading.selected.overallFeedback(testState),
+ );
+ });
+
+ test('selectors.app.rubric.feedbackPrompt', () => {
+ expect(mapped.feedbackPrompt).toEqual(
+ selectors.app.rubric.feedbackPrompt(testState),
+ );
+ });
+ });
+
+ describe('mapDispatchToProps', () => {
+ test('maps actions.grading.setRubricFeedback to setValue prop', () => {
+ expect(mapDispatchToProps.setValue).toEqual(
+ actions.grading.setRubricFeedback,
+ );
+ });
+ });
+});
diff --git a/src/containers/Rubric/__snapshots__/RubricFeedback.test.jsx.snap b/src/containers/Rubric/__snapshots__/RubricFeedback.test.jsx.snap
new file mode 100644
index 0000000..c021a2b
--- /dev/null
+++ b/src/containers/Rubric/__snapshots__/RubricFeedback.test.jsx.snap
@@ -0,0 +1,57 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Review Feedback component snapshot is configure to disabled 1`] = `null`;
+
+exports[`Review Feedback component snapshot is graded 1`] = `
+
+
+
+
+`;
+
+exports[`Review Feedback component snapshot is grading 1`] = `
+
+
+
+
+`;
diff --git a/src/containers/Rubric/__snapshots__/index.test.jsx.snap b/src/containers/Rubric/__snapshots__/index.test.jsx.snap
new file mode 100644
index 0000000..16b1da3
--- /dev/null
+++ b/src/containers/Rubric/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,92 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Rubric Container snapshot is grading 1`] = `
+
+
+
+ Rubric
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Rubric Container snapshot is not grading 1`] = `
+
+
+
+ Rubric
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/containers/Rubric/index.jsx b/src/containers/Rubric/index.jsx
index f1b3665..55859c7 100644
--- a/src/containers/Rubric/index.jsx
+++ b/src/containers/Rubric/index.jsx
@@ -2,10 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import {
- Card,
- Button,
-} from '@edx/paragon';
+import { Card, Button } from '@edx/paragon';
import selectors from 'data/selectors';
@@ -17,21 +14,22 @@ import './Rubric.scss';
/**
*
*/
-export const Rubric = ({
- isGrading,
- criteriaIndices,
-}) => (
+export const Rubric = ({ isGrading, criteriaIndices }) => (
Rubric
- { criteriaIndices.map(index => (
-
- )) }
+ {criteriaIndices.map((index) => (
+
+ ))}
- { isGrading && (
+ {isGrading && (
@@ -51,7 +49,6 @@ export const mapStateToProps = (state) => ({
criteriaIndices: selectors.app.rubric.criteriaIndices(state),
});
-export const mapDispatchToProps = {
-};
+export const mapDispatchToProps = {};
export default connect(mapStateToProps, mapDispatchToProps)(Rubric);
diff --git a/src/containers/Rubric/index.test.jsx b/src/containers/Rubric/index.test.jsx
new file mode 100644
index 0000000..f8a894e
--- /dev/null
+++ b/src/containers/Rubric/index.test.jsx
@@ -0,0 +1,91 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import selectors from 'data/selectors';
+import { Rubric, mapStateToProps } from '.';
+
+jest.mock('containers/CriterionContainer', () => 'CriterionContainer');
+jest.mock('./RubricFeedback', () => 'RubricFeedback');
+
+jest.mock('@edx/paragon', () => {
+ const Card = () => 'Card';
+ Card.Body = () => 'Card.Body';
+ const Button = () => 'Button';
+ return { Button, Card };
+});
+
+jest.mock('data/selectors', () => ({
+ __esModule: true,
+ default: {
+ app: {
+ isGrading: jest.fn((...args) => ({ isGragrding: args })),
+ rubric: {
+ criteriaIndices: jest.fn((...args) => ({
+ rubricCriteriaIndices: args,
+ })),
+ },
+ },
+ },
+}));
+
+describe('Rubric Container', () => {
+ const props = {
+ isGrading: true,
+ criteriaIndices: [1, 2, 3, 4, 5],
+ };
+ let el;
+ beforeEach(() => {
+ el = shallow();
+ });
+ describe('snapshot', () => {
+ test('is grading', () => {
+ expect(el).toMatchSnapshot();
+ });
+ test('is not grading', () => {
+ el.setProps({
+ isGrading: false,
+ });
+ expect(el).toMatchSnapshot();
+ });
+ });
+
+ describe('component', () => {
+ test('is grading (grading footer present)', () => {
+ expect(el.find('.grading-rubric-footer').length).toEqual(1);
+ const containers = el.find('CriterionContainer');
+ expect(containers.length).toEqual(props.criteriaIndices.length);
+ containers.forEach((container, i) => {
+ expect(container.key()).toEqual(String(props.criteriaIndices[i]));
+ });
+ });
+
+ test('is not grading (no grading footer)', () => {
+ el.setProps({
+ isGrading: false,
+ });
+ expect(el.find('.grading-rubric-footer').length).toEqual(0);
+ const containers = el.find('CriterionContainer');
+ expect(containers.length).toEqual(props.criteriaIndices.length);
+ containers.forEach((container, i) => {
+ expect(container.key()).toEqual(String(props.criteriaIndices[i]));
+ });
+ });
+ });
+
+ describe('mapStateToProps', () => {
+ const testState = { abitaryState: 'some data' };
+ let mapped;
+ beforeEach(() => {
+ mapped = mapStateToProps(testState);
+ });
+ test('selectors.app.isGrading', () => {
+ expect(mapped.isGrading).toEqual(selectors.app.isGrading(testState));
+ });
+
+ test('selectors.app.rubric.criteriaIndices', () => {
+ expect(mapped.criteriaIndices).toEqual(
+ selectors.app.rubric.criteriaIndices(testState),
+ );
+ });
+ });
+});
diff --git a/src/data/selectors/app.js b/src/data/selectors/app.js
index 2d18113..01118b6 100644
--- a/src/data/selectors/app.js
+++ b/src/data/selectors/app.js
@@ -71,6 +71,12 @@ rubric.hasConfig = rubricConfigSelector(config => config !== undefined);
*/
rubric.feedbackConfig = rubricConfigSelector(config => config.feedback);
+/**
+ * Return the criteria feedbase prompt
+ * @return {string} - criteria feedback prompt
+ */
+rubric.feedbackPrompt = rubricConfigSelector(config => config.feedbackPrompt);
+
/**
* Returns a list of rubric criterion config objects for the ORA
* @return {obj[]} - array of criterion config objects
diff --git a/src/data/services/lms/fakeData/ora.js b/src/data/services/lms/fakeData/ora.js
index 31f522b..9fd62f3 100644
--- a/src/data/services/lms/fakeData/ora.js
+++ b/src/data/services/lms/fakeData/ora.js
@@ -12,6 +12,7 @@ export const type = 'individual';
const rubricConfig = {
feedback: 'optional',
+ feedbackPrompt: 'Grader-facing prompt for submission-level feedback',
criteria: [
{
name: 'firstCriterion',