Skip to content

Commit 50d2870

Browse files
committed
feat: interactive rubric
chore: remove smart camelize chore: update logic and rewrite in typescript chore: revert back to jsx chore: rename api.ts to data.ts chore: update test
1 parent 57a5036 commit 50d2870

37 files changed

+1988
-595
lines changed

package-lock.json

Lines changed: 481 additions & 423 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
1717
"snapshot": "fedx-scripts jest --updateSnapshot",
1818
"start": "fedx-scripts webpack-dev-server --progress",
19-
"test": "fedx-scripts jest --coverage --passWithNoTests"
19+
"test": "fedx-scripts jest --coverage --passWithNoTests",
20+
"test:debug": "node --inspect-brk node_modules/.bin/fedx-scripts jest --runInBand --watch"
2021
},
2122
"husky": {
2223
"hooks": {
@@ -73,6 +74,6 @@
7374
"glob": "7.2.3",
7475
"husky": "7.0.4",
7576
"jest": "29.7.0",
76-
"ts-jest": "^29.0.0"
77+
"ts-jest": "^26.5.6"
7778
}
7879
}

src/components/Rubric/CriterionContainer/CriterionFeedback.jsx

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,41 @@ import PropTypes from 'prop-types';
33

44
import { Form } from '@edx/paragon';
55
import { useIntl } from '@edx/frontend-platform/i18n';
6-
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
76

87
import { feedbackRequirement } from 'data/services/lms/constants';
9-
import messages from './messages';
108

11-
export const stateKeys = StrictDict({
12-
value: 'value',
13-
});
9+
import messages from './messages';
1410

1511
/**
1612
* <CriterionFeedback />
1713
*/
18-
const CriterionFeedback = ({
19-
criterion,
20-
}) => {
21-
const [value, setValue] = useKeyedState(stateKeys.value, '');
14+
const CriterionFeedback = ({ criterion }) => {
2215
const { formatMessage } = useIntl();
2316

17+
let commentMessage = formatMessage(messages.addComments);
18+
if (criterion.feedbackRequired === feedbackRequirement.optional) {
19+
commentMessage += ` ${formatMessage(messages.optional)}`;
20+
}
21+
22+
const { feedbackValue, feedbackIsInvalid, feedbackOnChange } = criterion;
23+
2424
if (
2525
!criterion.feedbackEnabled
2626
|| criterion.feedbackRequired === feedbackRequirement.disabled
2727
) {
2828
return null;
2929
}
3030

31-
const onChange = ({ target }) => { setValue(target.value); };
32-
let commentMessage = formatMessage(messages.addComments);
33-
if (criterion.feedbackRequired === feedbackRequirement.optional) {
34-
commentMessage += ` ${formatMessage(messages.optional)}`;
35-
}
36-
37-
const isInvalid = value === '';
38-
3931
return (
40-
<Form.Group isInvalid={isInvalid}>
32+
<Form.Group isInvalid={feedbackIsInvalid}>
4133
<Form.Control
4234
as="textarea"
4335
className="criterion-feedback feedback-input"
4436
floatingLabel={commentMessage}
45-
value={value}
46-
onChange={onChange}
37+
value={feedbackValue}
38+
onChange={feedbackOnChange}
4739
/>
48-
{isInvalid && (
40+
{feedbackIsInvalid && (
4941
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
5042
{formatMessage(messages.criterionFeedbackError)}
5143
</Form.Control.Feedback>
@@ -56,8 +48,11 @@ const CriterionFeedback = ({
5648

5749
CriterionFeedback.propTypes = {
5850
criterion: PropTypes.shape({
59-
feedbackEnabled: PropTypes.bool,
60-
feedbackRequired: PropTypes.string,
51+
feedbackValue: PropTypes.string.isRequired,
52+
feedbackIsInvalid: PropTypes.bool.isRequired,
53+
feedbackOnChange: PropTypes.func.isRequired,
54+
feedbackEnabled: PropTypes.bool.isRequired,
55+
feedbackRequired: PropTypes.oneOf(Object.values(feedbackRequirement)).isRequired,
6156
}).isRequired,
6257
};
6358

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React from 'react';
2+
import { shallow } from '@edx/react-unit-test-utils';
3+
import { feedbackRequirement } from 'data/services/lms/constants';
4+
5+
import CriterionFeedback from './CriterionFeedback';
6+
7+
describe('<CriterionFeedback />', () => {
8+
const props = {
9+
criterion: {
10+
feedbackValue: 'feedback-1',
11+
feedbackIsInvalid: false,
12+
feedbackOnChange: jest.fn().mockName('feedbackOnChange'),
13+
feedbackEnabled: true,
14+
feedbackRequired: feedbackRequirement.required,
15+
},
16+
};
17+
describe('renders', () => {
18+
test('feedbackEnabled', () => {
19+
const wrapper = shallow(<CriterionFeedback {...props} />);
20+
expect(wrapper.snapshot).toMatchSnapshot();
21+
});
22+
23+
test('feedbackDisabled render empty', () => {
24+
const wrapper = shallow(
25+
<CriterionFeedback
26+
{...props}
27+
criterion={{ ...props.criterion, feedbackEnabled: false }}
28+
/>,
29+
);
30+
expect(wrapper.snapshot).toMatchSnapshot();
31+
expect(wrapper.isEmptyRender()).toBe(true);
32+
});
33+
34+
test('feedbackRequired disabled render empty', () => {
35+
const wrapper = shallow(
36+
<CriterionFeedback
37+
{...props}
38+
criterion={{
39+
...props.criterion,
40+
feedbackRequired: feedbackRequirement.disabled,
41+
}}
42+
/>,
43+
);
44+
expect(wrapper.snapshot).toMatchSnapshot();
45+
expect(wrapper.isEmptyRender()).toBe(true);
46+
});
47+
48+
test('feedbackRequired: optional', () => {
49+
const wrapper = shallow(
50+
<CriterionFeedback
51+
{...props}
52+
criterion={{
53+
...props.criterion,
54+
feedbackRequired: feedbackRequirement.optional,
55+
}}
56+
/>,
57+
);
58+
expect(wrapper.snapshot).toMatchSnapshot();
59+
expect(wrapper.instance.findByType('Form.Control')[0].props.floatingLabel).toContain('Optional');
60+
});
61+
62+
test('feedbackIsInvalid', () => {
63+
const wrapper = shallow(
64+
<CriterionFeedback
65+
{...props}
66+
criterion={{ ...props.criterion, feedbackIsInvalid: true }}
67+
/>,
68+
);
69+
expect(wrapper.snapshot).toMatchSnapshot();
70+
expect(wrapper.instance.findByType('Form.Control.Feedback')[0].props.type).toBe('invalid');
71+
});
72+
});
73+
});

src/components/Rubric/CriterionContainer/RadioCriterion.jsx

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,44 +3,37 @@ import PropTypes from 'prop-types';
33

44
import { Form } from '@edx/paragon';
55
import { useIntl } from '@edx/frontend-platform/i18n';
6-
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
76

87
import messages from './messages';
98

10-
export const stateKeys = StrictDict({
11-
value: 'value',
12-
});
13-
149
/**
1510
* <RadioCriterion />
1611
*/
17-
const RadioCriterion = ({
18-
isGrading,
19-
criterion,
20-
}) => {
21-
const [value, setValue] = useKeyedState(stateKeys.value, '');
12+
const RadioCriterion = ({ isGrading, criterion }) => {
2213
const { formatMessage } = useIntl();
23-
const onChange = ({ target }) => { setValue(target.value); };
24-
const isInvalid = value === '';
14+
15+
const { optionsValue, optionsIsInvalid, optionsOnChange } = criterion;
2516

2617
return (
27-
<Form.RadioSet name={criterion.name} value={value}>
18+
<Form.RadioSet name={criterion.name} value={optionsValue}>
2819
{criterion.options.map((option) => (
2920
<Form.Radio
3021
className="criteria-option"
3122
key={option.name}
3223
value={option.name}
33-
description={formatMessage(messages.optionPoints, { points: option.points })}
34-
onChange={onChange}
24+
description={formatMessage(messages.optionPoints, {
25+
points: option.points,
26+
})}
27+
onChange={optionsOnChange}
3528
disabled={!isGrading}
3629
>
3730
{option.name}
3831
</Form.Radio>
3932
))}
40-
{isInvalid && (
41-
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
42-
{formatMessage(messages.rubricSelectedError)}
43-
</Form.Control.Feedback>
33+
{optionsIsInvalid && (
34+
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
35+
{formatMessage(messages.rubricSelectedError)}
36+
</Form.Control.Feedback>
4437
)}
4538
</Form.RadioSet>
4639
);
@@ -49,12 +42,16 @@ const RadioCriterion = ({
4942
RadioCriterion.propTypes = {
5043
isGrading: PropTypes.bool.isRequired,
5144
criterion: PropTypes.shape({
52-
name: PropTypes.string,
53-
options: PropTypes.arrayOf(PropTypes.shape({
54-
name: PropTypes.string,
55-
description: PropTypes.string,
56-
points: PropTypes.number,
57-
})),
45+
optionsValue: PropTypes.string.isRequired,
46+
optionsIsInvalid: PropTypes.bool.isRequired,
47+
optionsOnChange: PropTypes.func.isRequired,
48+
name: PropTypes.string.isRequired,
49+
options: PropTypes.arrayOf(
50+
PropTypes.shape({
51+
name: PropTypes.string.isRequired,
52+
points: PropTypes.number.isRequired,
53+
}),
54+
).isRequired,
5855
}).isRequired,
5956
};
6057

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React from 'react';
2+
import { shallow } from '@edx/react-unit-test-utils';
3+
4+
import RadioCriterion from './RadioCriterion';
5+
6+
describe('<RadioCriterion />', () => {
7+
const props = {
8+
isGrading: true,
9+
criterion: {
10+
name: 'criterion-1',
11+
optionsValue: 'option-1',
12+
optionsIsInvalid: true,
13+
optionsOnChange: jest.fn().mockName('optionsOnChange'),
14+
options: [
15+
{
16+
name: 'option-1',
17+
description: 'description-1',
18+
points: 1,
19+
},
20+
{
21+
name: 'option-2',
22+
description: 'description-2',
23+
points: 2,
24+
},
25+
],
26+
},
27+
};
28+
describe('renders', () => {
29+
test('options is invalid', () => {
30+
const wrapper = shallow(<RadioCriterion {...props} />);
31+
expect(wrapper.snapshot).toMatchSnapshot();
32+
33+
expect(wrapper.instance.findByType('Form.Radio').length).toEqual(
34+
props.criterion.options.length,
35+
);
36+
wrapper.instance.findByType('Form.Radio').forEach((radio) => {
37+
expect(radio.props.disabled).toEqual(false);
38+
});
39+
expect(
40+
wrapper.instance.findByType('Form.Control.Feedback')[0].props.type,
41+
).toEqual('invalid');
42+
});
43+
44+
test('options is valid no invalid feedback get render', () => {
45+
const wrapper = shallow(
46+
<RadioCriterion
47+
{...props}
48+
criterion={{ ...props.criterion, optionsIsInvalid: false }}
49+
/>,
50+
);
51+
expect(wrapper.snapshot).toMatchSnapshot();
52+
53+
expect(
54+
wrapper.instance.findByType('Form.Control.Feedback').length,
55+
).toEqual(0);
56+
});
57+
58+
test('not isGrading all radios will be disabled', () => {
59+
const wrapper = shallow(<RadioCriterion {...props} isGrading={false} />);
60+
expect(wrapper.snapshot).toMatchSnapshot();
61+
62+
wrapper.instance.findByType('Form.Radio').forEach((radio) => {
63+
expect(radio.props.disabled).toEqual(true);
64+
});
65+
});
66+
});
67+
});

src/components/Rubric/CriterionContainer/ReviewCriterion.jsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,10 @@ ReviewCriterion.propTypes = {
2828
criterion: PropTypes.shape({
2929
options: PropTypes.arrayOf(
3030
PropTypes.shape({
31-
description: PropTypes.string,
32-
name: PropTypes.string,
33-
points: PropTypes.number,
31+
name: PropTypes.string.isRequired,
32+
points: PropTypes.number.isRequired,
3433
}),
35-
),
34+
).isRequired,
3635
}).isRequired,
3736
};
3837

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from 'react';
2+
import { shallow } from '@edx/react-unit-test-utils';
3+
4+
import ReviewCriterion from './ReviewCriterion';
5+
6+
describe('<ReviewCriterion />', () => {
7+
const props = {
8+
criterion: {
9+
options: [
10+
{
11+
name: 'option-1',
12+
description: 'description-1',
13+
points: 1,
14+
},
15+
{
16+
name: 'option-2',
17+
description: 'description-2',
18+
points: 2,
19+
},
20+
],
21+
},
22+
};
23+
24+
test('renders', () => {
25+
const wrapper = shallow(<ReviewCriterion {...props} />);
26+
expect(wrapper.snapshot).toMatchSnapshot();
27+
expect(wrapper.instance.findByType('FormControlFeedback').length).toEqual(props.criterion.options.length);
28+
});
29+
});

0 commit comments

Comments
 (0)