Rubric feedback (#9)
* add feedbackPrompt to selector * create info popover component and radio criterion * update criterion container to use new component instead * remove unused components * update rubric feedback to use gradeState * update to test for bound method * update info popover and the test * implement review criterion * remove gradeStatus from rubric feedback and criterion feedback * use gradeStatus to show review criterion * update selector testing naming
This commit is contained in:
45
src/components/InfoPopover.jsx
Normal file
45
src/components/InfoPopover.jsx
Normal file
@@ -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';
|
||||
|
||||
/**
|
||||
* <InfoPopover />
|
||||
*/
|
||||
export const InfoPopover = ({ children }) => (
|
||||
<OverlayTrigger
|
||||
trigger="focus"
|
||||
placement="auto"
|
||||
flip
|
||||
overlay={
|
||||
<Popover className="overlay-help-popover">
|
||||
<PopoverContent>{children}</PopoverContent>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
className="criteria-help-icon"
|
||||
src={InfoOutline}
|
||||
alt="criterion info"
|
||||
iconAs={Icon}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
|
||||
InfoPopover.defaultProps = {};
|
||||
|
||||
InfoPopover.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
export default InfoPopover;
|
||||
34
src/components/InfoPopover.test.jsx
Normal file
34
src/components/InfoPopover.test.jsx
Normal file
@@ -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 = <div>Children component</div>;
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<InfoPopover>{child}</InfoPopover>)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<InfoPopover>{child}</InfoPopover>);
|
||||
});
|
||||
test('Test component render', () => {
|
||||
expect(el.length).toEqual(1);
|
||||
expect(el.find('.criteria-help-icon').length).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
27
src/components/__snapshots__/InfoPopover.test.jsx.snap
Normal file
27
src/components/__snapshots__/InfoPopover.test.jsx.snap
Normal file
@@ -0,0 +1,27 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Info Popover Component snapshot 1`] = `
|
||||
<OverlayTrigger
|
||||
flip={true}
|
||||
overlay={
|
||||
<Popover
|
||||
className="overlay-help-popover"
|
||||
>
|
||||
<PopoverContent>
|
||||
<div>
|
||||
Children component
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
}
|
||||
placement="auto"
|
||||
trigger="focus"
|
||||
>
|
||||
<IconButton
|
||||
alt="criterion info"
|
||||
className="criteria-help-icon"
|
||||
iconAs={[MockFunction Icon]}
|
||||
src={[MockFunction icons.InfoOutline]}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
`;
|
||||
@@ -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
|
||||
? (
|
||||
<Form.Control
|
||||
as="input"
|
||||
className="criterion-feedback feedback-input"
|
||||
floatingLabel="Add comments"
|
||||
value={this.props.value}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Form.Text className="feedback-text">{this.props.value}</Form.Text>
|
||||
);
|
||||
return (
|
||||
<Form.Control
|
||||
as="input"
|
||||
className="criterion-feedback feedback-input"
|
||||
floatingLabel={isGrading ? 'Add comments' : 'Comments'}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
disabled={!isGrading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }),
|
||||
});
|
||||
|
||||
142
src/containers/CriterionContainer/CriterionFeedback.test.jsx
Normal file
142
src/containers/CriterionContainer/CriterionFeedback.test.jsx
Normal file
@@ -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(<CriterionFeedback {...props} />);
|
||||
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(<CriterionFeedback {...props} />);
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
* <OptionInfoPopover />
|
||||
*/
|
||||
export const OptionInfoPopover = ({ options }) => (
|
||||
<OverlayTrigger
|
||||
trigger="focus"
|
||||
placement="left"
|
||||
overlay={(
|
||||
<Popover className="overlay-help-popover">
|
||||
<Popover.Content>
|
||||
{options.map(option => (
|
||||
<div key={option.name} className="help-popover-option">
|
||||
<strong>{option.label}</strong><br />
|
||||
{option.explanation}
|
||||
</div>
|
||||
))}
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
className="criteria-help-icon"
|
||||
onClick={() => {}}
|
||||
src={InfoOutline}
|
||||
alt="criterion info"
|
||||
iconAs={Icon}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
|
||||
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;
|
||||
@@ -8,9 +8,9 @@ import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
/**
|
||||
* <GradingCriterion />
|
||||
* <RadioCriterion />
|
||||
*/
|
||||
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 (
|
||||
<>
|
||||
<Form.RadioSet
|
||||
name={config.name}
|
||||
value={data.selectedOption || ''}
|
||||
>
|
||||
{ config.options.map(option => (
|
||||
<Form.RadioSet name={config.name} value={data.selectedOption || ''}>
|
||||
{config.options.map((option) => (
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
key={option.name}
|
||||
value={option.name}
|
||||
description={`${option.points} points`}
|
||||
onChange={this.onChange}
|
||||
disabled={!isGrading}
|
||||
>
|
||||
{option.label}
|
||||
</Form.Radio>
|
||||
)) }
|
||||
))}
|
||||
</Form.RadioSet>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
153
src/containers/CriterionContainer/RadioCriterion.test.jsx
Normal file
153
src/containers/CriterionContainer/RadioCriterion.test.jsx
Normal file
@@ -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(<RadioCriterion {...props} />);
|
||||
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(<RadioCriterion {...props} />);
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
* <ReviewCriterion />
|
||||
*/
|
||||
export const ReviewCriterion = ({
|
||||
config,
|
||||
// data,
|
||||
}) => (
|
||||
export const ReviewCriterion = ({ config }) => (
|
||||
<div className="review-criterion">
|
||||
{ config.options.map(option => (
|
||||
{config.options.map((option) => (
|
||||
<div key={option.name} className="criteria-option">
|
||||
<div>
|
||||
<Form.Label className="option-label">
|
||||
{option.label}
|
||||
</Form.Label>
|
||||
<Form.Label className="option-label">{option.label}</Form.Label>
|
||||
<FormControlFeedback className="option-points">
|
||||
{`${option.points} points`}
|
||||
</FormControlFeedback>
|
||||
</div>
|
||||
</div>
|
||||
)) }
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
110
src/containers/CriterionContainer/ReviewCriterion.test.jsx
Normal file
110
src/containers/CriterionContainer/ReviewCriterion.test.jsx
Normal file
@@ -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(<ReviewCriterion {...props} />);
|
||||
});
|
||||
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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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`] = `
|
||||
<Control
|
||||
as="input"
|
||||
className="criterion-feedback feedback-input"
|
||||
disabled={true}
|
||||
floatingLabel="Comments"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="some value"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Criterion Feedback snapshot is grading 1`] = `
|
||||
<Control
|
||||
as="input"
|
||||
className="criterion-feedback feedback-input"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="some value"
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,57 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Radio Crition Container snapshot is grading 1`] = `
|
||||
<React.Fragment>
|
||||
<RadioSet
|
||||
name="random name"
|
||||
value="selected option"
|
||||
>
|
||||
<Radio
|
||||
className="criteria-option"
|
||||
description="1 points"
|
||||
disabled={false}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name"
|
||||
>
|
||||
this label
|
||||
</Radio>
|
||||
<Radio
|
||||
className="criteria-option"
|
||||
description="2 points"
|
||||
disabled={false}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name 2"
|
||||
>
|
||||
this label 2
|
||||
</Radio>
|
||||
</RadioSet>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`Radio Crition Container snapshot is not grading 1`] = `
|
||||
<React.Fragment>
|
||||
<RadioSet
|
||||
name="random name"
|
||||
value="selected option"
|
||||
>
|
||||
<Radio
|
||||
className="criteria-option"
|
||||
description="1 points"
|
||||
disabled={true}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name"
|
||||
>
|
||||
this label
|
||||
</Radio>
|
||||
<Radio
|
||||
className="criteria-option"
|
||||
description="2 points"
|
||||
disabled={true}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name 2"
|
||||
>
|
||||
this label 2
|
||||
</Radio>
|
||||
</RadioSet>
|
||||
</React.Fragment>
|
||||
`;
|
||||
@@ -0,0 +1,42 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Review Crition Container snapshot 1`] = `
|
||||
<div
|
||||
className="review-criterion"
|
||||
>
|
||||
<div
|
||||
className="criteria-option"
|
||||
key="option name"
|
||||
>
|
||||
<div>
|
||||
<Label
|
||||
className="option-label"
|
||||
>
|
||||
this label
|
||||
</Label>
|
||||
<FormControlFeedback
|
||||
className="option-points"
|
||||
>
|
||||
1 points
|
||||
</FormControlFeedback>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="criteria-option"
|
||||
key="option name 2"
|
||||
>
|
||||
<div>
|
||||
<Label
|
||||
className="option-label"
|
||||
>
|
||||
this label 2
|
||||
</Label>
|
||||
<FormControlFeedback
|
||||
className="option-points"
|
||||
>
|
||||
2 points
|
||||
</FormControlFeedback>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,144 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Criterion Container snapshot is graded and is not grading 1`] = `
|
||||
<Group>
|
||||
<Label
|
||||
className="criteria-label"
|
||||
>
|
||||
<span
|
||||
className="criteria-title"
|
||||
>
|
||||
prompt
|
||||
</span>
|
||||
<InfoPopover>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
key="option name"
|
||||
>
|
||||
<strong>
|
||||
this label
|
||||
</strong>
|
||||
<br />
|
||||
explaination
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
key="option name 2"
|
||||
>
|
||||
<strong>
|
||||
this label 2
|
||||
</strong>
|
||||
<br />
|
||||
explaination 2
|
||||
</div>
|
||||
</InfoPopover>
|
||||
</Label>
|
||||
<div
|
||||
className="rubric-criteria"
|
||||
>
|
||||
<RadioCriterion
|
||||
isGrading={false}
|
||||
orderNum={1}
|
||||
/>
|
||||
</div>
|
||||
<CriterionFeedback
|
||||
isGrading={false}
|
||||
orderNum={1}
|
||||
/>
|
||||
</Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Container snapshot is ungraded and is grading 1`] = `
|
||||
<Group>
|
||||
<Label
|
||||
className="criteria-label"
|
||||
>
|
||||
<span
|
||||
className="criteria-title"
|
||||
>
|
||||
prompt
|
||||
</span>
|
||||
<InfoPopover>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
key="option name"
|
||||
>
|
||||
<strong>
|
||||
this label
|
||||
</strong>
|
||||
<br />
|
||||
explaination
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
key="option name 2"
|
||||
>
|
||||
<strong>
|
||||
this label 2
|
||||
</strong>
|
||||
<br />
|
||||
explaination 2
|
||||
</div>
|
||||
</InfoPopover>
|
||||
</Label>
|
||||
<div
|
||||
className="rubric-criteria"
|
||||
>
|
||||
<RadioCriterion
|
||||
isGrading={true}
|
||||
orderNum={1}
|
||||
/>
|
||||
</div>
|
||||
<CriterionFeedback
|
||||
isGrading={true}
|
||||
orderNum={1}
|
||||
/>
|
||||
</Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Container snapshot is ungraded and is not grading 1`] = `
|
||||
<Group>
|
||||
<Label
|
||||
className="criteria-label"
|
||||
>
|
||||
<span
|
||||
className="criteria-title"
|
||||
>
|
||||
prompt
|
||||
</span>
|
||||
<InfoPopover>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
key="option name"
|
||||
>
|
||||
<strong>
|
||||
this label
|
||||
</strong>
|
||||
<br />
|
||||
explaination
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
key="option name 2"
|
||||
>
|
||||
<strong>
|
||||
this label 2
|
||||
</strong>
|
||||
<br />
|
||||
explaination 2
|
||||
</div>
|
||||
</InfoPopover>
|
||||
</Label>
|
||||
<div
|
||||
className="rubric-criteria"
|
||||
>
|
||||
<ReviewCriterion
|
||||
orderNum={1}
|
||||
/>
|
||||
</div>
|
||||
<CriterionFeedback
|
||||
isGrading={false}
|
||||
orderNum={1}
|
||||
/>
|
||||
</Group>
|
||||
`;
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
* <CriterionContainer />
|
||||
*/
|
||||
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 (
|
||||
<Form.Group>
|
||||
<Form.Label className="criteria-label">
|
||||
<span className="criteria-title">
|
||||
{config.prompt}
|
||||
</span>
|
||||
<OptionInfoPopover options={config.options} />
|
||||
<span className="criteria-title">{config.prompt}</span>
|
||||
<InfoPopover>
|
||||
{config.options.map((option) => (
|
||||
<div key={option.name} className="help-popover-option">
|
||||
<strong>{option.label}</strong>
|
||||
<br />
|
||||
{option.explanation}
|
||||
</div>
|
||||
))}
|
||||
</InfoPopover>
|
||||
</Form.Label>
|
||||
<div className="rubric-criteria">
|
||||
{
|
||||
isGrading
|
||||
? <GradingCriterion orderNum={orderNum} />
|
||||
: <ReviewCriterion orderNum={orderNum} />
|
||||
}
|
||||
{isGrading || gradeStatus === gradeStatuses.graded ? (
|
||||
<RadioCriterion orderNum={orderNum} isGrading={isGrading} />
|
||||
) : (
|
||||
<ReviewCriterion orderNum={orderNum} />
|
||||
)}
|
||||
</div>
|
||||
<CriterionFeedback orderNum={orderNum} />
|
||||
<CriterionFeedback orderNum={orderNum} isGrading={isGrading} />
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
144
src/containers/CriterionContainer/index.test.jsx
Normal file
144
src/containers/CriterionContainer/index.test.jsx
Normal file
@@ -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(<CriterionContainer {...props} />);
|
||||
});
|
||||
|
||||
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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
* <RubricFeedback />
|
||||
@@ -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 (
|
||||
<Form.Group>
|
||||
<Form.Label className="criteria-label">
|
||||
<span className="criteria-title">Overall comments</span>
|
||||
<InfoPopover>
|
||||
<div>{feedbackPrompt}</div>
|
||||
</InfoPopover>
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="input"
|
||||
className="rubric-feedback feedback-input"
|
||||
floatingLabel="Add comments"
|
||||
value={this.props.value}
|
||||
floatingLabel={isGrading ? 'Add comments' : 'Comments'}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
disabled={!isGrading}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Form.Text className="feedback-text">{this.props.value}</Form.Text>
|
||||
);
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
160
src/containers/Rubric/RubricFeedback.test.jsx
Normal file
160
src/containers/Rubric/RubricFeedback.test.jsx
Normal file
@@ -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(<RubricFeedback {...props} />);
|
||||
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(<RubricFeedback {...props} />);
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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`] = `
|
||||
<Group>
|
||||
<Label
|
||||
className="criteria-label"
|
||||
>
|
||||
<span
|
||||
className="criteria-title"
|
||||
>
|
||||
Overall comments
|
||||
</span>
|
||||
<InfoPopover>
|
||||
<div>
|
||||
feedback prompt
|
||||
</div>
|
||||
</InfoPopover>
|
||||
</Label>
|
||||
<Control
|
||||
as="input"
|
||||
className="rubric-feedback feedback-input"
|
||||
disabled={true}
|
||||
floatingLabel="Comments"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="some value"
|
||||
/>
|
||||
</Group>
|
||||
`;
|
||||
|
||||
exports[`Review Feedback component snapshot is grading 1`] = `
|
||||
<Group>
|
||||
<Label
|
||||
className="criteria-label"
|
||||
>
|
||||
<span
|
||||
className="criteria-title"
|
||||
>
|
||||
Overall comments
|
||||
</span>
|
||||
<InfoPopover>
|
||||
<div>
|
||||
feedback prompt
|
||||
</div>
|
||||
</InfoPopover>
|
||||
</Label>
|
||||
<Control
|
||||
as="input"
|
||||
className="rubric-feedback feedback-input"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="some value"
|
||||
/>
|
||||
</Group>
|
||||
`;
|
||||
92
src/containers/Rubric/__snapshots__/index.test.jsx.snap
Normal file
92
src/containers/Rubric/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,92 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Rubric Container snapshot is grading 1`] = `
|
||||
<Card
|
||||
className="grading-rubric-card"
|
||||
>
|
||||
<Component
|
||||
className="grading-rubric-body"
|
||||
>
|
||||
<h3>
|
||||
Rubric
|
||||
</h3>
|
||||
<hr />
|
||||
<CriterionContainer
|
||||
isGrading={true}
|
||||
key="1"
|
||||
orderNum={1}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={true}
|
||||
key="2"
|
||||
orderNum={2}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={true}
|
||||
key="3"
|
||||
orderNum={3}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={true}
|
||||
key="4"
|
||||
orderNum={4}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={true}
|
||||
key="5"
|
||||
orderNum={5}
|
||||
/>
|
||||
<hr />
|
||||
<RubricFeedback />
|
||||
</Component>
|
||||
<div
|
||||
className="grading-rubric-footer"
|
||||
>
|
||||
<Button>
|
||||
Submit Grade
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
`;
|
||||
|
||||
exports[`Rubric Container snapshot is not grading 1`] = `
|
||||
<Card
|
||||
className="grading-rubric-card"
|
||||
>
|
||||
<Component
|
||||
className="grading-rubric-body"
|
||||
>
|
||||
<h3>
|
||||
Rubric
|
||||
</h3>
|
||||
<hr />
|
||||
<CriterionContainer
|
||||
isGrading={false}
|
||||
key="1"
|
||||
orderNum={1}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={false}
|
||||
key="2"
|
||||
orderNum={2}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={false}
|
||||
key="3"
|
||||
orderNum={3}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={false}
|
||||
key="4"
|
||||
orderNum={4}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={false}
|
||||
key="5"
|
||||
orderNum={5}
|
||||
/>
|
||||
<hr />
|
||||
<RubricFeedback />
|
||||
</Component>
|
||||
</Card>
|
||||
`;
|
||||
@@ -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';
|
||||
/**
|
||||
* <Rubric />
|
||||
*/
|
||||
export const Rubric = ({
|
||||
isGrading,
|
||||
criteriaIndices,
|
||||
}) => (
|
||||
export const Rubric = ({ isGrading, criteriaIndices }) => (
|
||||
<Card className="grading-rubric-card">
|
||||
<Card.Body className="grading-rubric-body">
|
||||
<h3>Rubric</h3>
|
||||
<hr />
|
||||
{ criteriaIndices.map(index => (
|
||||
<CriterionContainer isGrading={isGrading} key={index} orderNum={index} />
|
||||
)) }
|
||||
{criteriaIndices.map((index) => (
|
||||
<CriterionContainer
|
||||
isGrading={isGrading}
|
||||
key={index}
|
||||
orderNum={index}
|
||||
/>
|
||||
))}
|
||||
<hr />
|
||||
<RubricFeedback />
|
||||
</Card.Body>
|
||||
{ isGrading && (
|
||||
{isGrading && (
|
||||
<div className="grading-rubric-footer">
|
||||
<Button>Submit Grade</Button>
|
||||
</div>
|
||||
@@ -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);
|
||||
|
||||
91
src/containers/Rubric/index.test.jsx
Normal file
91
src/containers/Rubric/index.test.jsx
Normal file
@@ -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(<Rubric {...props} />);
|
||||
});
|
||||
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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -12,6 +12,7 @@ export const type = 'individual';
|
||||
|
||||
const rubricConfig = {
|
||||
feedback: 'optional',
|
||||
feedbackPrompt: 'Grader-facing prompt for submission-level feedback',
|
||||
criteria: [
|
||||
{
|
||||
name: 'firstCriterion',
|
||||
|
||||
Reference in New Issue
Block a user